diff --git a/src/assets/styles/tailwind.css b/src/assets/styles/tailwind.css index 96aabe5..6d5c5f5 100644 --- a/src/assets/styles/tailwind.css +++ b/src/assets/styles/tailwind.css @@ -1,16 +1,16 @@ @tailwind base; - @tailwind components; - @tailwind utilities; +@tailwind components; +@tailwind utilities; - @layer base { +@layer base { p, li, div { line-height: 1.5; } - } +} - @layer utilities { +@layer utilities { .bg-page { background-color: var(--aw-color-bg-page); } @@ -26,78 +26,71 @@ .text-muted { color: var(--aw-color-text-muted); } - } +} - @layer components { +@layer components { .btn { - @apply inline-flex items-center justify-center rounded-full border-gray-400 border bg-transparent font-medium text-center text-base text-page leading-snug transition py-3.5 px-6 md:px-8 ease-in duration-200 focus:ring-blue-500 focus:ring-offset-blue-200 focus:ring-2 focus:ring-offset-2 hover:bg-gray-100 hover:border-gray-600 dark:text-slate-300 dark:border-slate-500 dark:hover:bg-slate-800 dark:hover:border-slate-800 cursor-pointer; + @apply inline-flex items-center justify-center rounded-full border-gray-400 border bg-transparent font-medium text-center text-base text-page leading-snug transition py-3.5 px-6 md:px-8 ease-in duration-200 focus:ring-blue-500 focus:ring-offset-blue-200 focus:ring-2 focus:ring-offset-2 hover:bg-gray-100 hover:border-gray-600 dark:text-slate-300 dark:border-slate-500 dark:hover:bg-slate-800 dark:hover:border-slate-800 cursor-pointer; } .btn-primary { - @apply btn font-semibold bg-primary text-white border-primary hover:bg-secondary hover:border-secondary hover:text-white dark:text-white dark:bg-primary dark:border-primary dark:hover:border-secondary dark:hover:bg-secondary; + @apply btn font-semibold bg-primary text-white border-primary hover:bg-secondary hover:border-secondary hover:text-white dark:text-white dark:bg-primary dark:border-primary dark:hover:border-secondary dark:hover:bg-secondary; } .btn-secondary { - @apply btn; + @apply btn; } .btn-tertiary { - @apply btn border-none shadow-none text-muted hover:text-gray-900 dark:text-gray-400 dark:hover:text-white; + @apply btn border-none shadow-none text-muted hover:text-gray-900 dark:text-gray-400 dark:hover:text-white; } - } +} - #header.scroll > div:first-child { +#header.scroll > div:first-child { @apply bg-page md:bg-white/90 md:backdrop-blur-md; box-shadow: 0 0.375rem 1.5rem 0 rgb(140 152 164 / 13%); - } - .dark #header.scroll > div:first-child, - #header.scroll.dark > div:first-child { +} + +.dark #header.scroll > div:first-child, +#header.scroll.dark > div:first-child { @apply bg-page md:bg-[#030621e6] border-b border-gray-500/20; box-shadow: none; - } - /* #header.scroll > div:last-child { - @apply py-3; - } */ +} - #header.expanded nav { +#header.expanded nav { position: fixed; top: 70px; left: 0; right: 0; bottom: 70px !important; padding: 0 5px; - } +} - .dropdown:focus .dropdown-menu, - .dropdown:focus-within .dropdown-menu, - .dropdown:hover .dropdown-menu { +.dropdown:focus .dropdown-menu, +.dropdown:focus-within .dropdown-menu, +.dropdown:hover .dropdown-menu { display: block; - } +} - [astro-icon].icon-light > * { +[astro-icon].icon-light > * { stroke-width: 1.2; - } +} - [astro-icon].icon-bold > * { +[astro-icon].icon-bold > * { stroke-width: 2.4; - } +} - [data-aw-toggle-menu] path { +[data-aw-toggle-menu] path { @apply transition; - } - [data-aw-toggle-menu].expanded g > path:first-child { +} + +[data-aw-toggle-menu].expanded g > path:first-child { @apply -rotate-45 translate-y-[15px] translate-x-[-3px]; - } +} - [data-aw-toggle-menu].expanded g > path:last-child { +[data-aw-toggle-menu].expanded g > path:last-child { @apply rotate-45 translate-y-[-8px] translate-x-[14px]; - } - - /* To deprecated */ - - .dd *:first-child { - margin-top: 0; - } +} /* Custom Tippy.js theme for light/dark mode */ .tippy-box[data-theme~='devnet'] { @@ -108,11 +101,13 @@ font-size: 1rem; padding: 0.75rem 1rem; } + .dark .tippy-box[data-theme~='devnet'] { background-color: #23272f; color: #f3f4f6; box-shadow: 0 4px 24px rgba(0,0,0,0.40); } + .tippy-box[data-theme~='devnet'] .tippy-arrow { color: inherit; } diff --git a/src/components/blog/Grid.astro b/src/components/blog/Grid.astro deleted file mode 100644 index 1b62be4..0000000 --- a/src/components/blog/Grid.astro +++ /dev/null @@ -1,14 +0,0 @@ ---- -import Item from '~/components/blog/GridItem.astro'; -import type { Post } from '~/types'; - -export interface Props { - posts: Array; -} - -const { posts } = Astro.props; ---- - -
- {posts.map((post) => )} -
diff --git a/src/components/blog/GridItem.astro b/src/components/blog/GridItem.astro deleted file mode 100644 index cd02fa8..0000000 --- a/src/components/blog/GridItem.astro +++ /dev/null @@ -1,71 +0,0 @@ ---- -import { APP_BLOG } from 'astrowind:config'; -import type { Post } from '~/types'; - -import Image from '~/components/common/Image.astro'; - -import { findImage } from '~/utils/images'; -import { getPermalink } from '~/utils/permalinks'; - -export interface Props { - post: Post; -} - -const { post } = Astro.props; -const image = await findImage(post.image); - -const link = APP_BLOG?.post?.isEnabled ? getPermalink(post.permalink, 'post') : ''; ---- - -
-
- { - image && - (link ? ( - - {post.title} - - ) : ( - {post.title} - )) - } -
- -

- { - link ? ( - - {post.title} - - ) : ( - post.title - ) - } -

- -

{post.excerpt}

-
diff --git a/src/components/blog/Headline.astro b/src/components/blog/Headline.astro deleted file mode 100644 index 5d3ccc6..0000000 --- a/src/components/blog/Headline.astro +++ /dev/null @@ -1,12 +0,0 @@ ---- -const { title = await Astro.slots.render('default'), subtitle = await Astro.slots.render('subtitle') } = Astro.props; ---- - -
-

- { - subtitle && ( -
- ) - } -

diff --git a/src/components/blog/List.astro b/src/components/blog/List.astro deleted file mode 100644 index 6a80ae3..0000000 --- a/src/components/blog/List.astro +++ /dev/null @@ -1,20 +0,0 @@ ---- -import Item from '~/components/blog/ListItem.astro'; -import type { Post } from '~/types'; - -export interface Props { - posts: Array; -} - -const { posts } = Astro.props; ---- - - diff --git a/src/components/blog/ListItem.astro b/src/components/blog/ListItem.astro deleted file mode 100644 index 6a416d6..0000000 --- a/src/components/blog/ListItem.astro +++ /dev/null @@ -1,120 +0,0 @@ ---- -import type { ImageMetadata } from 'astro'; -import { Icon } from 'astro-icon/components'; -import Image from '~/components/common/Image.astro'; -import PostTags from '~/components/blog/Tags.astro'; - -import { APP_BLOG } from 'astrowind:config'; -import type { Post } from '~/types'; - -import { getPermalink } from '~/utils/permalinks'; -import { findImage } from '~/utils/images'; -import { getFormattedDate } from '~/utils/utils'; - -export interface Props { - post: Post; -} - -const { post } = Astro.props; -const image = (await findImage(post.image)) as ImageMetadata | undefined; - -const link = APP_BLOG?.post?.isEnabled ? getPermalink(post.permalink, 'post') : ''; ---- - -
- { - image && - (link ? ( - - - - ) : ( - - )) - } -
-
-
- - - - { - post.author && ( - <> - {' '} - · - {post.author.replaceAll('-', ' ')} - - ) - } - { - post.category && ( - <> - {' '} - ·{' '} - - {post.category.title} - - - ) - } - -
-

- { - link ? ( - - {post.title} - - ) : ( - post.title - ) - } -

-
- - {post.excerpt &&

{post.excerpt}

} - { - post.tags && Array.isArray(post.tags) ? ( -
- -
- ) : ( - - ) - } -
-
diff --git a/src/components/blog/Pagination.astro b/src/components/blog/Pagination.astro deleted file mode 100644 index 051587c..0000000 --- a/src/components/blog/Pagination.astro +++ /dev/null @@ -1,36 +0,0 @@ ---- -import { Icon } from 'astro-icon/components'; -import { getPermalink } from '~/utils/permalinks'; -import Button from '~/components/ui/Button.astro'; - -export interface Props { - prevUrl?: string; - nextUrl?: string; - prevText?: string; - nextText?: string; -} - -const { prevUrl, nextUrl, prevText = 'Newer posts', nextText = 'Older posts' } = Astro.props; ---- - -{ - (prevUrl || nextUrl) && ( -
-
- - - -
-
- ) -} diff --git a/src/components/blog/RelatedPosts.astro b/src/components/blog/RelatedPosts.astro deleted file mode 100644 index f4036e9..0000000 --- a/src/components/blog/RelatedPosts.astro +++ /dev/null @@ -1,31 +0,0 @@ ---- -import { APP_BLOG } from 'astrowind:config'; - -import { getRelatedPosts } from '~/utils/blog'; -import BlogHighlightedPosts from '../widgets/BlogHighlightedPosts.astro'; -import type { Post } from '~/types'; -import { getBlogPermalink } from '~/utils/permalinks'; - -export interface Props { - post: Post; -} - -const { post } = Astro.props; - -const relatedPosts = post.tags ? await getRelatedPosts(post, 4) : []; ---- - -{ - APP_BLOG.isRelatedPostsEnabled ? ( - post.id)} - /> - ) : null -} diff --git a/src/components/blog/SinglePost.astro b/src/components/blog/SinglePost.astro deleted file mode 100644 index 297cca9..0000000 --- a/src/components/blog/SinglePost.astro +++ /dev/null @@ -1,103 +0,0 @@ ---- -import { Icon } from 'astro-icon/components'; - -import Image from '~/components/common/Image.astro'; -import PostTags from '~/components/blog/Tags.astro'; -import SocialShare from '~/components/common/SocialShare.astro'; - -import { getPermalink } from '~/utils/permalinks'; -import { getFormattedDate } from '~/utils/utils'; - -import type { Post } from '~/types'; - -export interface Props { - post: Post; - url: string | URL; -} - -const { post, url } = Astro.props; ---- - -
-
-
-
-

- - - { - post.author && ( - <> - {' '} - · - {post.author} - - ) - } - { - post.category && ( - <> - {' '} - ·{' '} - - {post.category.title} - - - ) - } - { - post.readingTime && ( - <> -  · {post.readingTime} min read - - ) - } -

-
- -

- {post.title} -

-

- {post.excerpt} -

- - { - post.image ? ( - {post?.excerpt - ) : ( -
-
-
- ) - } -
-
- -
-
- - -
-
-
diff --git a/src/components/blog/Tags.astro b/src/components/blog/Tags.astro deleted file mode 100644 index ae46a24..0000000 --- a/src/components/blog/Tags.astro +++ /dev/null @@ -1,43 +0,0 @@ ---- -import { getPermalink } from '~/utils/permalinks'; - -import { APP_BLOG } from 'astrowind:config'; -import type { Post } from '~/types'; - -export interface Props { - tags: Post['tags']; - class?: string; - title?: string | undefined; - isCategory?: boolean; -} - -const { tags, class: className = 'text-sm', title = undefined, isCategory = false } = Astro.props; ---- - -{ - tags && Array.isArray(tags) && ( - <> - {title !== undefined && ( - - {title} - - )} -
    - {tags.map((tag) => ( -
  • - {!APP_BLOG?.tag?.isEnabled ? ( - tag.title - ) : ( - - {tag.title} - - )} -
  • - ))} -
- - ) -} diff --git a/src/components/blog/ToBlogLink.astro b/src/components/blog/ToBlogLink.astro deleted file mode 100644 index 7fb7a49..0000000 --- a/src/components/blog/ToBlogLink.astro +++ /dev/null @@ -1,20 +0,0 @@ ---- -import { Icon } from 'astro-icon/components'; -import { getBlogPermalink } from '~/utils/permalinks'; -import { I18N } from 'astrowind:config'; -import Button from '~/components/ui/Button.astro'; - -const { textDirection } = I18N; ---- - -
- -
diff --git a/src/components/widgets/Announcement.astro b/src/components/widgets/Announcement.astro deleted file mode 100644 index eb9ec4b..0000000 --- a/src/components/widgets/Announcement.astro +++ /dev/null @@ -1,23 +0,0 @@ ---- - ---- - - diff --git a/src/components/widgets/BlogHighlightedPosts.astro b/src/components/widgets/BlogHighlightedPosts.astro deleted file mode 100644 index 75f35a9..0000000 --- a/src/components/widgets/BlogHighlightedPosts.astro +++ /dev/null @@ -1,64 +0,0 @@ ---- -import { APP_BLOG } from 'astrowind:config'; - -import Grid from '~/components/blog/Grid.astro'; - -import { getBlogPermalink } from '~/utils/permalinks'; -import { findPostsByIds } from '~/utils/blog'; -import WidgetWrapper from '~/components/ui/WidgetWrapper.astro'; -import type { Widget } from '~/types'; - -export interface Props extends Widget { - title?: string; - linkText?: string; - linkUrl?: string | URL; - information?: string; - postIds: string[]; -} - -const { - title = await Astro.slots.render('title'), - linkText = 'View all posts', - linkUrl = getBlogPermalink(), - information = await Astro.slots.render('information'), - postIds = [], - - id, - isDark = false, - classes = {}, - bg = await Astro.slots.render('bg'), -} = Astro.props; - -const posts = APP_BLOG.isEnabled ? await findPostsByIds(postIds) : []; ---- - -{ - APP_BLOG.isEnabled ? ( - -
- {title && ( -
-

- {APP_BLOG.list.isEnabled && linkText && linkUrl && ( - - {linkText} » - - )} -

- )} - - {information &&

} -

- - -
- ) : ( - - ) -} diff --git a/src/components/widgets/BlogLatestPosts.astro b/src/components/widgets/BlogLatestPosts.astro deleted file mode 100644 index 8694797..0000000 --- a/src/components/widgets/BlogLatestPosts.astro +++ /dev/null @@ -1,65 +0,0 @@ ---- -import { APP_BLOG } from 'astrowind:config'; - -import Grid from '~/components/blog/Grid.astro'; - -import { getBlogPermalink } from '~/utils/permalinks'; -import { findLatestPosts } from '~/utils/blog'; -import WidgetWrapper from '~/components/ui/WidgetWrapper.astro'; -import type { Widget } from '~/types'; -import Button from '../ui/Button.astro'; - -export interface Props extends Widget { - title?: string; - linkText?: string; - linkUrl?: string | URL; - information?: string; - count?: number; -} - -const { - title = await Astro.slots.render('title'), - linkText = 'View all posts', - linkUrl = getBlogPermalink(), - information = await Astro.slots.render('information'), - count = 4, - - id, - isDark = false, - classes = {}, - bg = await Astro.slots.render('bg'), -} = Astro.props; - -const posts = APP_BLOG.isEnabled ? await findLatestPosts({ count }) : []; ---- - -{ - APP_BLOG.isEnabled ? ( - -
- {title && ( -
-

- {APP_BLOG.list.isEnabled && linkText && linkUrl && ( - - )} -

- )} - - {information && ( -

- )} -

- - -
- ) : ( - - ) -} diff --git a/src/components/widgets/Brands.astro b/src/components/widgets/Brands.astro deleted file mode 100644 index 7e42ae1..0000000 --- a/src/components/widgets/Brands.astro +++ /dev/null @@ -1,38 +0,0 @@ ---- -import { Icon } from 'astro-icon/components'; -import type { Brands as Props } from '~/types'; - -import Image from '~/components/common/Image.astro'; -import Headline from '~/components/ui/Headline.astro'; -import WidgetWrapper from '~/components/ui/WidgetWrapper.astro'; -const { - title = '', - subtitle = '', - tagline = '', - icons = [], - images = [], - id, - isDark = false, - classes = {}, - bg = await Astro.slots.render('bg'), -} = Astro.props; ---- - - - - -
- {icons && icons.map((icon) => )} - { - images && - images.map( - (image) => - image.src && ( -
- {image.alt -
- ) - ) - } -
-
diff --git a/src/components/widgets/FAQs.astro b/src/components/widgets/FAQs.astro deleted file mode 100644 index bd289d5..0000000 --- a/src/components/widgets/FAQs.astro +++ /dev/null @@ -1,24 +0,0 @@ ---- -import Headline from '~/components/ui/Headline.astro'; -import Accordion from '~/components/ui/Accordion.astro'; -import WidgetWrapper from '~/components/ui/WidgetWrapper.astro'; -import type { Faqs as Props } from '~/types'; - -const { - title = '', - subtitle = '', - tagline = '', - items = [], - columns = 2, - - id, - isDark = false, - classes = {}, - bg = await Astro.slots.render('bg'), -} = Astro.props; ---- - - - - - diff --git a/src/components/widgets/Features3.astro b/src/components/widgets/Features3.astro deleted file mode 100644 index 62ab475..0000000 --- a/src/components/widgets/Features3.astro +++ /dev/null @@ -1,70 +0,0 @@ ---- -import Headline from '~/components/ui/Headline.astro'; -import ItemGrid from '~/components/ui/ItemGrid.astro'; -import WidgetWrapper from '~/components/ui/WidgetWrapper.astro'; -import Image from '~/components/common/Image.astro'; -import type { Features as Props } from '~/types'; - -const { - title = await Astro.slots.render('title'), - subtitle = await Astro.slots.render('subtitle'), - tagline = await Astro.slots.render('tagline'), - image, - items = [], - columns, - defaultIcon, - isBeforeContent, - isAfterContent, - - id, - isDark = false, - classes = {}, - bg = await Astro.slots.render('bg'), -} = Astro.props; ---- - - - } /> - - - - - diff --git a/src/components/widgets/Note.astro b/src/components/widgets/Note.astro deleted file mode 100644 index 6543b4f..0000000 --- a/src/components/widgets/Note.astro +++ /dev/null @@ -1,11 +0,0 @@ ---- -import { Icon } from 'astro-icon/components'; ---- - -
-
- - Philosophy: Simplicity, Best Practices and High Performance -
-
diff --git a/src/components/widgets/Pricing.astro b/src/components/widgets/Pricing.astro deleted file mode 100644 index 3f20b74..0000000 --- a/src/components/widgets/Pricing.astro +++ /dev/null @@ -1,83 +0,0 @@ ---- -import { Icon } from 'astro-icon/components'; -import Button from '~/components/ui/Button.astro'; -import Headline from '~/components/ui/Headline.astro'; -import WidgetWrapper from '~/components/ui/WidgetWrapper.astro'; -import type { Pricing as Props } from '~/types'; - -const { - title = '', - subtitle = '', - tagline = '', - prices = [], - - id, - isDark = false, - classes = {}, - bg = await Astro.slots.render('bg'), -} = Astro.props; ---- - - - -
-
- { - prices && - prices.map(({ title, subtitle, price, period, items, callToAction, hasRibbon = false, ribbonTitle }) => ( -
- {price && period && ( -
- {hasRibbon && ribbonTitle && ( -
- - {ribbonTitle} - -
- )} -
- {title && ( -

{title}

- )} - {subtitle &&

{subtitle}

} -
-
- $ - {price} -
- {period} -
- {items && ( -
    - {items.map( - ({ description, icon }) => - description && ( -
  • -
    - -
    - {description} -
  • - ) - )} -
- )} -
- {callToAction && ( -
- {typeof callToAction === 'string' ? ( - - ) : ( - callToAction && - callToAction.href &&
- )} -
- )} -
- )) - } -
-
-
diff --git a/src/components/widgets/Stats.astro b/src/components/widgets/Stats.astro deleted file mode 100644 index 07d3465..0000000 --- a/src/components/widgets/Stats.astro +++ /dev/null @@ -1,46 +0,0 @@ ---- -import type { Stats as Props } from '~/types'; -import WidgetWrapper from '../ui/WidgetWrapper.astro'; -import Headline from '../ui/Headline.astro'; -import { Icon } from 'astro-icon/components'; - -const { - title = await Astro.slots.render('title'), - subtitle = await Astro.slots.render('subtitle'), - tagline, - stats = [], - - id, - isDark = false, - classes = {}, - bg = await Astro.slots.render('bg'), -} = Astro.props; ---- - - - -
- { - stats && - stats.map(({ amount, title, icon }) => ( -
- {icon && ( -
- -
- )} - {amount && ( -
- {amount} -
- )} - {title && ( -
- {title} -
- )} -
- )) - } -
-
diff --git a/src/components/widgets/Steps.astro b/src/components/widgets/Steps.astro deleted file mode 100644 index 3c65bf6..0000000 --- a/src/components/widgets/Steps.astro +++ /dev/null @@ -1,59 +0,0 @@ ---- -import WidgetWrapper from '~/components/ui/WidgetWrapper.astro'; -import Timeline from '~/components/ui/Timeline.astro'; -import Headline from '~/components/ui/Headline.astro'; -import Image from '~/components/common/Image.astro'; -import type { Steps as Props } from '~/types'; - -const { - title = await Astro.slots.render('title'), - subtitle = await Astro.slots.render('subtitle'), - tagline = await Astro.slots.render('tagline'), - items = [], - image = await Astro.slots.render('image'), - isReversed = false, - - id, - isDark = false, - classes = {}, - bg = await Astro.slots.render('bg'), -} = Astro.props; ---- - - -
-
- - } /> -
- { - image && ( -
- {typeof image === 'string' ? ( - - ) : ( - {image?.alt - )} -
- ) - } -
-
diff --git a/src/components/widgets/Steps2.astro b/src/components/widgets/Steps2.astro deleted file mode 100644 index 8f7afb5..0000000 --- a/src/components/widgets/Steps2.astro +++ /dev/null @@ -1,72 +0,0 @@ ---- -import { Icon } from 'astro-icon/components'; -import WidgetWrapper from '~/components/ui/WidgetWrapper.astro'; -import Headline from '~/components/ui/Headline.astro'; -import Button from '~/components/ui/Button.astro'; -import type { Steps as Props } from '~/types'; - -const { - title = await Astro.slots.render('title'), - subtitle = await Astro.slots.render('subtitle'), - tagline, - callToAction = await Astro.slots.render('callToAction'), - items = [], - isReversed = false, - - id, - isDark = false, - classes = {}, - bg = await Astro.slots.render('bg'), -} = Astro.props; ---- - - -
-
- - -
- { - typeof callToAction === 'string' ? ( - - ) : ( - callToAction && - callToAction.text && - callToAction.href &&
-
-
-
    - { - items && items.length - ? items.map(({ title: title2, description, icon }, index) => ( -
  • -
    - - {icon ? : index + 1} - -
    -
    -

    -

    -

    -
  • - )) - : '' - } -
-
-
-
diff --git a/src/config.yaml b/src/config.yaml index dc6ff50..53b12ed 100644 --- a/src/config.yaml +++ b/src/config.yaml @@ -29,38 +29,6 @@ i18n: language: en textDirection: ltr -apps: - blog: - isEnabled: true - postsPerPage: 6 - - post: - isEnabled: true - permalink: 'blog/%slug%' # Variables: %slug%, %year%, %month%, %day%, %hour%, %minute%, %second%, %category% - robots: - index: true - - list: - isEnabled: true - pathname: 'blog' # Blog main path, you can change this to "articles" (/articles) - robots: - index: true - - category: - isEnabled: true - pathname: 'category' # Category main path /category/some-category, you can change this to "group" (/group/some-category) - robots: - index: true - - tag: - isEnabled: true - pathname: 'tag' # Tag main path /tag/some-tag, you can change this to "topics" (/topics/some-category) - robots: - index: false - - isRelatedPostsEnabled: true - relatedPostsCount: 4 - analytics: vendors: googleAnalytics: diff --git a/src/content/config.ts b/src/content/config.ts deleted file mode 100644 index 71bc2f5..0000000 --- a/src/content/config.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { z, defineCollection } from 'astro:content'; -import { glob } from 'astro/loaders'; - -const metadataDefinition = () => - z - .object({ - title: z.string().optional(), - ignoreTitleTemplate: z.boolean().optional(), - - canonical: z.string().url().optional(), - - robots: z - .object({ - index: z.boolean().optional(), - follow: z.boolean().optional(), - }) - .optional(), - - description: z.string().optional(), - - openGraph: z - .object({ - url: z.string().optional(), - siteName: z.string().optional(), - images: z - .array( - z.object({ - url: z.string(), - width: z.number().optional(), - height: z.number().optional(), - }) - ) - .optional(), - locale: z.string().optional(), - type: z.string().optional(), - }) - .optional(), - - twitter: z - .object({ - handle: z.string().optional(), - site: z.string().optional(), - cardType: z.string().optional(), - }) - .optional(), - }) - .optional(); - -const postCollection = defineCollection({ - loader: glob({ pattern: ['*.md', '*.mdx'], base: 'src/data/post' }), - schema: z.object({ - publishDate: z.date().optional(), - updateDate: z.date().optional(), - draft: z.boolean().optional(), - - title: z.string(), - excerpt: z.string().optional(), - image: z.string().optional(), - - category: z.string().optional(), - tags: z.array(z.string()).optional(), - author: z.string().optional(), - - metadata: metadataDefinition(), - }), -}); - -export const collections = { - post: postCollection, -}; diff --git a/src/email-templates/README.md b/src/email-templates/README.md deleted file mode 100644 index 1e34724..0000000 --- a/src/email-templates/README.md +++ /dev/null @@ -1,72 +0,0 @@ -# Email Handling System - -This directory contains the email templates and utilities for the contact form email handling system. - -## Features - -- **Secure SMTP Authentication**: Uses environment variables for credentials -- **Email Templates**: Customizable templates for both user confirmation and admin notification emails -- **Rate Limiting**: Prevents abuse by limiting the number of submissions per IP address -- **CSRF Protection**: Prevents cross-site request forgery attacks -- **Email Validation**: Ensures valid email addresses are provided -- **Spam Prevention**: Multiple checks to detect and block spam submissions -- **Error Handling**: Proper error handling with client feedback -- **Logging**: Comprehensive logging of email sending attempts - -## Configuration - -The email system is configured using environment variables in the `.env` file: - -``` -# SMTP Configuration -SMTP_HOST=smtp.example.com -SMTP_PORT=587 -SMTP_USER=your-email@example.com -SMTP_PASS=your-password - -# Email Settings -ADMIN_EMAIL=admin@example.com -WEBSITE_NAME=Your Website Name - -# Environment -NODE_ENV=development -``` - -In development mode, emails are logged to the console instead of being sent. Set `NODE_ENV=production` to send actual emails. - -## Files - -- `admin-notification.ts`: Template for emails sent to the admin -- `user-confirmation.ts`: Template for confirmation emails sent to users -- `../utils/email-handler.ts`: Core email handling functionality - -## How It Works - -1. When a user submits the contact form, the client-side JavaScript validates the form and sends it to the `/api/contact` endpoint. -2. The endpoint validates the form data, checks for CSRF token validity, and performs rate limiting and spam detection. -3. If all checks pass, two emails are sent: - - A notification email to the admin with the form data - - A confirmation email to the user acknowledging receipt of their message -4. The system logs all email sending attempts for monitoring and debugging. - -## Development vs. Production - -- In development mode (`NODE_ENV=development`), emails are logged to the console instead of being sent. -- In production mode (`NODE_ENV=production`), emails are sent using the configured SMTP server. - -## Security Considerations - -- SMTP credentials are stored in environment variables, not in the code -- CSRF tokens are used to prevent cross-site request forgery -- Rate limiting prevents abuse of the contact form -- Form data is validated both on the client and server side -- Spam detection helps prevent unwanted messages - -## Testing - -To test the email system: - -1. Configure the `.env` file with your SMTP settings -2. Submit the contact form on the website -3. Check the logs for email sending attempts -4. In production mode, check your inbox for the actual emails diff --git a/src/email-templates/admin-notification.ts b/src/email-templates/admin-notification.ts deleted file mode 100644 index 11d1f69..0000000 --- a/src/email-templates/admin-notification.ts +++ /dev/null @@ -1,111 +0,0 @@ -interface AdminNotificationProps { - name: string; - email: string; - message: string; - submittedAt: string; - ipAddress?: string; - userAgent?: string; -} - -export function getAdminNotificationSubject(): string { - return `New Contact Form Submission from ${process.env.WEBSITE_NAME || '365devnet.eu'}`; -} - -export function getAdminNotificationHtml(props: AdminNotificationProps): string { - const { name, email, message, submittedAt, ipAddress, userAgent } = props; - - return ` - - - - - - New Contact Form Submission - - - -
-

New Contact Form Submission

-
-
-

From: ${name} (${email})

-

Submitted on: ${submittedAt}

- -
-

Message:

-

${message.replace(/\n/g, '
')}

-
- -
-

Additional Information:

-

IP Address: ${ipAddress || 'Not available'}

-

User Agent: ${userAgent || 'Not available'}

-
-
- - - - `; -} - -export function getAdminNotificationText(props: AdminNotificationProps): string { - const { name, email, message, submittedAt, ipAddress, userAgent } = props; - - return ` -New Contact Form Submission - -From: ${name} (${email}) -Submitted on: ${submittedAt} - -Message: -${message} - -Additional Information: -IP Address: ${ipAddress || 'Not available'} -User Agent: ${userAgent || 'Not available'} - -This is an automated email from your website contact form. - `; -} diff --git a/src/email-templates/user-confirmation.ts b/src/email-templates/user-confirmation.ts deleted file mode 100644 index 5c4cd83..0000000 --- a/src/email-templates/user-confirmation.ts +++ /dev/null @@ -1,120 +0,0 @@ -interface UserConfirmationProps { - name: string; - email: string; - message: string; - submittedAt: string; - websiteName?: string; - contactEmail?: string; -} - -export function getUserConfirmationSubject(websiteName: string = process.env.WEBSITE_NAME || '365devnet.eu'): string { - return `Thank you for contacting ${websiteName}`; -} - -export function getUserConfirmationHtml(props: UserConfirmationProps): string { - const { - name, - message, - submittedAt, - websiteName = process.env.WEBSITE_NAME || '365devnet.eu', - contactEmail = process.env.ADMIN_EMAIL || 'richard@bergsma.it', - } = props; - - return ` - - - - - - Thank you for contacting us - - - -
-

Thank you for contacting ${websiteName}

-
-
-

Dear ${name},

- -

Thank you for reaching out to us. We have received your message and will get back to you as soon as possible.

- -
-

Your message (submitted on ${submittedAt}):

-

${message.replace(/\n/g, '
')}

-
- -

If you have any additional questions or information to provide, please feel free to reply to this email.

- -

Best regards,
- The ${websiteName} Team

-
- - - - `; -} - -export function getUserConfirmationText(props: UserConfirmationProps): string { - const { - name, - message, - submittedAt, - websiteName = process.env.WEBSITE_NAME || '365devnet.eu', - contactEmail = process.env.ADMIN_EMAIL || 'richard@bergsma.it', - } = props; - - return ` -Thank you for contacting ${websiteName} - -Dear ${name}, - -Thank you for reaching out to us. We have received your message and will get back to you as soon as possible. - -Your message (submitted on ${submittedAt}): -${message} - -If you have any additional questions or information to provide, please feel free to reply to this email. - -Best regards, -The ${websiteName} Team - -If you did not submit this contact form, please disregard this email or contact us at ${contactEmail}. - `; -} diff --git a/src/layouts/LandingLayout.astro b/src/layouts/LandingLayout.astro deleted file mode 100644 index 0554afa..0000000 --- a/src/layouts/LandingLayout.astro +++ /dev/null @@ -1,35 +0,0 @@ ---- -import PageLayout from '~/layouts/PageLayout.astro'; -import Header from '~/components/widgets/Header.astro'; - -import { headerData } from '~/navigation'; -import type { MetaData } from '~/types'; - -export interface Props { - metadata?: MetaData; -} - -const { metadata } = Astro.props; ---- - - - - - - - -
- - - - diff --git a/src/layouts/MarkdownLayout.astro b/src/layouts/MarkdownLayout.astro deleted file mode 100644 index ae6e9b0..0000000 --- a/src/layouts/MarkdownLayout.astro +++ /dev/null @@ -1,28 +0,0 @@ ---- -import Layout from '~/layouts/PageLayout.astro'; - -import type { MetaData } from '~/types'; - -export interface Props { - frontmatter: { - title?: string; - }; -} - -const { frontmatter } = Astro.props; - -const metadata: MetaData = { - title: frontmatter?.title, -}; ---- - - -
-

{frontmatter.title}

-
- -
-
-
diff --git a/src/pages/[...blog]/[...page].astro b/src/pages/[...blog]/[...page].astro deleted file mode 100644 index 5a6da41..0000000 --- a/src/pages/[...blog]/[...page].astro +++ /dev/null @@ -1,52 +0,0 @@ ---- -import type { InferGetStaticPropsType, GetStaticPaths } from 'astro'; - -import Layout from '~/layouts/PageLayout.astro'; -import BlogList from '~/components/blog/List.astro'; -import Headline from '~/components/blog/Headline.astro'; -import Pagination from '~/components/blog/Pagination.astro'; -// import PostTags from "~/components/blog/Tags.astro"; - -import { blogListRobots, getStaticPathsBlogList } from '~/utils/blog'; - -export const prerender = true; - -export const getStaticPaths = (async ({ paginate }) => { - return await getStaticPathsBlogList({ paginate }); -}) satisfies GetStaticPaths; - -type Props = InferGetStaticPropsType; - -const { page } = Astro.props as Props; -const currentPage = page.currentPage ?? 1; - -// const allCategories = await findCategories(); -// const allTags = await findTags(); - -const metadata = { - title: `Blog${currentPage > 1 ? ` — Page ${currentPage}` : ''}`, - robots: { - index: blogListRobots?.index && currentPage === 1, - follow: blogListRobots?.follow, - }, - openGraph: { - type: 'blog', - }, -}; ---- - - -
- - The Blog - - - - -
-
diff --git a/src/pages/[...blog]/[category]/[...page].astro b/src/pages/[...blog]/[category]/[...page].astro deleted file mode 100644 index e1c4ff6..0000000 --- a/src/pages/[...blog]/[category]/[...page].astro +++ /dev/null @@ -1,37 +0,0 @@ ---- -import type { InferGetStaticPropsType, GetStaticPaths } from 'astro'; -import { blogCategoryRobots, getStaticPathsBlogCategory } from '~/utils/blog'; - -import Layout from '~/layouts/PageLayout.astro'; -import BlogList from '~/components/blog/List.astro'; -import Headline from '~/components/blog/Headline.astro'; -import Pagination from '~/components/blog/Pagination.astro'; - -export const prerender = true; - -export const getStaticPaths = (async ({ paginate }) => { - return await getStaticPathsBlogCategory({ paginate }); -}) satisfies GetStaticPaths; - -type Props = InferGetStaticPropsType & { category: Record }; - -const { page, category } = Astro.props as Props; - -const currentPage = page.currentPage ?? 1; - -const metadata = { - title: `Category '${category.title}' ${currentPage > 1 ? ` — Page ${currentPage}` : ''}`, - robots: { - index: blogCategoryRobots?.index, - follow: blogCategoryRobots?.follow, - }, -}; ---- - - -
- {category.title} - - -
-
diff --git a/src/pages/[...blog]/[tag]/[...page].astro b/src/pages/[...blog]/[tag]/[...page].astro deleted file mode 100644 index 86a767b..0000000 --- a/src/pages/[...blog]/[tag]/[...page].astro +++ /dev/null @@ -1,37 +0,0 @@ ---- -import type { InferGetStaticPropsType, GetStaticPaths } from 'astro'; -import { blogTagRobots, getStaticPathsBlogTag } from '~/utils/blog'; - -import Layout from '~/layouts/PageLayout.astro'; -import BlogList from '~/components/blog/List.astro'; -import Headline from '~/components/blog/Headline.astro'; -import Pagination from '~/components/blog/Pagination.astro'; - -export const prerender = true; - -export const getStaticPaths = (async ({ paginate }) => { - return await getStaticPathsBlogTag({ paginate }); -}) satisfies GetStaticPaths; - -type Props = InferGetStaticPropsType; - -const { page, tag } = Astro.props as Props; - -const currentPage = page.currentPage ?? 1; - -const metadata = { - title: `Posts by tag '${tag.title}'${currentPage > 1 ? ` — Page ${currentPage} ` : ''}`, - robots: { - index: blogTagRobots?.index, - follow: blogTagRobots?.follow, - }, -}; ---- - - -
- Tag: {tag.title} - - -
-
diff --git a/src/pages/[...blog]/index.astro b/src/pages/[...blog]/index.astro deleted file mode 100644 index 73c7ba0..0000000 --- a/src/pages/[...blog]/index.astro +++ /dev/null @@ -1,59 +0,0 @@ ---- -import type { InferGetStaticPropsType, GetStaticPaths } from 'astro'; - -import merge from 'lodash.merge'; -import type { ImageMetadata } from 'astro'; -import Layout from '~/layouts/PageLayout.astro'; -import SinglePost from '~/components/blog/SinglePost.astro'; -import ToBlogLink from '~/components/blog/ToBlogLink.astro'; - -import { getCanonical, getPermalink } from '~/utils/permalinks'; -import { getStaticPathsBlogPost, blogPostRobots } from '~/utils/blog'; -import { findImage } from '~/utils/images'; -import type { MetaData } from '~/types'; -import RelatedPosts from '~/components/blog/RelatedPosts.astro'; -import { getBlogTranslation, supportedLanguages } from '~/i18n/translations.blog'; - -export const prerender = true; - -export const getStaticPaths = (async () => { - return await getStaticPathsBlogPost(); -}) satisfies GetStaticPaths; - -type Props = InferGetStaticPropsType; - -const { post } = Astro.props as Props; - -// Try to extract lang from post or fallback to 'en' -const lang = post?.lang && supportedLanguages.includes(post.lang) ? post.lang : 'en'; -const blogT = getBlogTranslation(lang); - -const url = getCanonical(getPermalink(post.permalink, 'post')); -const image = (await findImage(post.image)) as ImageMetadata | string | undefined; - -const metadata = merge( - { - title: blogT.title, - description: blogT.information, - robots: { - index: blogPostRobots?.index, - follow: blogPostRobots?.follow, - }, - openGraph: { - type: 'article', - ...(image - ? { images: [{ url: image, width: (image as ImageMetadata)?.width, height: (image as ImageMetadata)?.height }] } - : {}), - }, - }, - { ...(post?.metadata ? { ...post.metadata, canonical: post.metadata?.canonical || url } : {}) } -) as MetaData; ---- - - - - {post.Content ? : } - - - - diff --git a/src/utils/email-handler.ts b/src/utils/email-handler.ts index aee8e06..0d93c01 100644 --- a/src/utils/email-handler.ts +++ b/src/utils/email-handler.ts @@ -1,16 +1,6 @@ import nodemailer from 'nodemailer'; import { RateLimiterMemory } from 'rate-limiter-flexible'; import { createHash } from 'crypto'; -import { - getAdminNotificationHtml, - getAdminNotificationText, - getAdminNotificationSubject, -} from '../email-templates/admin-notification'; -import { - getUserConfirmationHtml, - getUserConfirmationText, - getUserConfirmationSubject, -} from '../email-templates/user-confirmation'; import 'dotenv/config'; // Environment variables @@ -205,67 +195,236 @@ export async function sendAdminNotification( return false; } - if (!ADMIN_EMAIL || ADMIN_EMAIL.trim() === '') { - console.error('Cannot send admin notification: ADMIN_EMAIL is not configured'); - return false; - } + const subject = `New Contact Form Submission from ${name}`; + const html = ` + + + + + + New Contact Form Submission + + + +
+

New Contact Form Submission

+
+
+
+
Name
+
${name}
+
+
+
Email
+
${email}
+
+
+
Message
+
${message.replace(/\n/g, '
')}
+
+
+ ${ipAddress ? `
IP Address: ${ipAddress}
` : ''} + ${userAgent ? `
User Agent: ${userAgent}
` : ''} +
Time: ${new Date().toLocaleString()}
+
+ +
+ + + `; + const text = ` +New Contact Form Submission - const submittedAt = new Date().toLocaleString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); +Name: ${name} +Email: ${email} +Message: +${message} +${ipAddress ? `IP Address: ${ipAddress}` : ''} +${userAgent ? `User Agent: ${userAgent}` : ''} +Time: ${new Date().toLocaleString()} - const props = { - name, - email, - message, - submittedAt, - ipAddress, - userAgent, - }; +This message was sent from the contact form on ${WEBSITE_NAME} + `; - const subject = getAdminNotificationSubject(); - const html = getAdminNotificationHtml(props); - const text = getAdminNotificationText(props); - - // Add a backup email address to ensure delivery - const recipients = ADMIN_EMAIL; - // Uncomment and modify the line below to add a backup email address - // const recipients = `${ADMIN_EMAIL}, your-backup-email@example.com`; - - return sendEmail(recipients, subject, html, text); + return sendEmail(ADMIN_EMAIL, subject, html, text); } // Send user confirmation email export async function sendUserConfirmation(name: string, email: string, message: string): Promise { + // Validate inputs + if (!name || name.trim() === '') { + console.error('Cannot send user confirmation: name is empty'); + return false; + } + if (!email || email.trim() === '') { console.error('Cannot send user confirmation: email is empty'); return false; } - const submittedAt = new Date().toLocaleString('en-US', { - year: 'numeric', - month: 'long', - day: 'numeric', - hour: '2-digit', - minute: '2-digit', - }); + const subject = `Thank you for contacting ${WEBSITE_NAME}`; + const html = ` + + + + + + Thank you for your message + + + +
+

Thank you for your message

+
+
+

Dear ${name},

+

Thank you for contacting ${WEBSITE_NAME}. We have received your message and will get back to you as soon as possible.

+ +
+

Your Message:

+

${message.replace(/\n/g, '
')}

+
- const props = { - name, - email, - message, - submittedAt, - websiteName: WEBSITE_NAME, - contactEmail: ADMIN_EMAIL, - }; +

If you have any additional information to share, please don't hesitate to reply to this email.

+ + Visit Our Website - const subject = getUserConfirmationSubject(WEBSITE_NAME); - const html = getUserConfirmationHtml(props); - const text = getUserConfirmationText(props); + +
+ + + `; + const text = ` +Thank you for your message + +Dear ${name}, + +Thank you for contacting ${WEBSITE_NAME}. We have received your message and will get back to you as soon as possible. + +Here's a copy of your message: + +${message} + +If you have any additional information to share, please don't hesitate to reply to this email. + +Best regards, +${WEBSITE_NAME} Team + +This is an automated message, please do not reply directly to this email. + `; return sendEmail(email, subject, html, text); } @@ -275,43 +434,17 @@ export function initializeEmailSystem(): void { initializeTransporter(); } -// Initialize on import -initializeEmailSystem(); - -// Test email function to verify configuration +// Test email configuration export async function testEmailConfiguration(): Promise { - if (!isProduction) { - return true; + if (!transporter) { + initializeTransporter(); } try { - // Initialize transporter if not already done - if (!transporter) { - initializeTransporter(); - } - - // Verify connection to SMTP server - const connectionResult = await new Promise((resolve) => { - transporter.verify(function (error, _success) { - if (error) { - resolve(false); - } else { - resolve(true); - } - }); - }); - - if (!connectionResult) { - return false; - } - + await transporter.verify(); return true; - } catch { + } catch (error) { + console.error('Email configuration test failed:', error); return false; } -} - -// Run a test of the email configuration -if (isProduction) { - testEmailConfiguration(); -} +} \ No newline at end of file