├── .markdownlint.json ├── public ├── robots.txt ├── images │ ├── logo.png │ ├── avatar.png │ ├── banner.png │ ├── favicon.png │ ├── avatar-sm.png │ ├── og-image.png │ ├── service-1.png │ ├── service-2.png │ ├── service-3.png │ ├── call-to-action.png │ ├── logo-darkmode.png │ └── image-placeholder.png └── .htaccess ├── .dockerignore ├── .vscode ├── extensions.json └── settings.json ├── netlify.toml ├── src ├── content │ ├── authors │ │ ├── -index.md │ │ ├── john-doe.md │ │ ├── sam-wilson.md │ │ └── william-jacob.md │ ├── blog │ │ ├── -index.md │ │ ├── post-3.md │ │ ├── post-4.md │ │ ├── post-1.md │ │ └── post-2.md │ ├── contact │ │ └── -index.md │ ├── sections │ │ ├── call-to-action.md │ │ └── testimonial.md │ ├── about │ │ └── -index.md │ ├── pages │ │ ├── privacy-policy.md │ │ └── elements.mdx │ └── homepage │ │ └── -index.md ├── layouts │ ├── shortcodes │ │ ├── Tab.tsx │ │ ├── Youtube.tsx │ │ ├── Video.tsx │ │ ├── Button.tsx │ │ ├── Accordion.tsx │ │ ├── Tabs.tsx │ │ └── Notice.tsx │ ├── helpers │ │ ├── Disqus.tsx │ │ ├── DynamicIcon.tsx │ │ ├── Announcement.tsx │ │ ├── SearchModal.tsx │ │ └── SearchResult.tsx │ ├── partials │ │ ├── PageHeader.astro │ │ ├── Footer.astro │ │ ├── CallToAction.astro │ │ ├── PostSidebar.astro │ │ ├── Testimonial.astro │ │ └── Header.astro │ ├── components │ │ ├── TwSizeIndicator.astro │ │ ├── Social.astro │ │ ├── AuthorCard.astro │ │ ├── Breadcrumbs.astro │ │ ├── ImageMod.astro │ │ ├── Share.astro │ │ ├── Logo.astro │ │ ├── BlogCard.astro │ │ ├── ThemeSwitcher.astro │ │ └── Pagination.astro │ ├── PostSingle.astro │ └── Base.astro ├── types │ └── index.d.ts ├── lib │ ├── utils │ │ ├── taxonomyFilter.ts │ │ ├── dateFormat.ts │ │ ├── sortFunctions.ts │ │ ├── readingTime.ts │ │ ├── similarItems.ts │ │ ├── bgImageMod.ts │ │ └── textConverter.ts │ ├── contentParser.astro │ └── taxonomyParser.astro ├── hooks │ └── useTheme.ts ├── styles │ ├── safe.css │ ├── main.css │ ├── buttons.css │ ├── utilities.css │ ├── base.css │ ├── generated-theme.css │ ├── navigation.css │ ├── search.css │ └── components.css ├── config │ ├── social.json │ ├── theme.json │ ├── menu.json │ └── config.json ├── pages │ ├── blog │ │ ├── [single].astro │ │ ├── index.astro │ │ └── page │ │ │ └── [slug].astro │ ├── 404.astro │ ├── authors │ │ ├── index.astro │ │ └── [single].astro │ ├── [regular].astro │ ├── tags │ │ ├── index.astro │ │ └── [tag].astro │ ├── about.astro │ ├── categories │ │ ├── index.astro │ │ └── [category].astro │ ├── contact.astro │ └── index.astro └── content.config.ts ├── .prettierrc ├── .editorconfig ├── .sitepins ├── config.json └── schema │ ├── authors.json │ └── blog.json ├── .gitignore ├── tsconfig.json ├── config └── nginx │ └── nginx.conf ├── LICENSE ├── astro.config.mjs ├── Dockerfile ├── package.json ├── scripts ├── jsonGenerator.js ├── removeDarkmode.js └── themeGenerator.js └── readme.md /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "MD033": false, 3 | "MD013": false 4 | } 5 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | 4 | Disallow: /api/* -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeon-studio/astroplate/HEAD/public/images/logo.png -------------------------------------------------------------------------------- /public/images/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeon-studio/astroplate/HEAD/public/images/avatar.png -------------------------------------------------------------------------------- /public/images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeon-studio/astroplate/HEAD/public/images/banner.png -------------------------------------------------------------------------------- /public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeon-studio/astroplate/HEAD/public/images/favicon.png -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .dockerignore 3 | node_modules 4 | npm-debug.log 5 | README.md 6 | .next 7 | .git -------------------------------------------------------------------------------- /public/images/avatar-sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeon-studio/astroplate/HEAD/public/images/avatar-sm.png -------------------------------------------------------------------------------- /public/images/og-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeon-studio/astroplate/HEAD/public/images/og-image.png -------------------------------------------------------------------------------- /public/images/service-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeon-studio/astroplate/HEAD/public/images/service-1.png -------------------------------------------------------------------------------- /public/images/service-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeon-studio/astroplate/HEAD/public/images/service-2.png -------------------------------------------------------------------------------- /public/images/service-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeon-studio/astroplate/HEAD/public/images/service-3.png -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode","bradlc.vscode-tailwindcss"] 3 | } 4 | -------------------------------------------------------------------------------- /public/images/call-to-action.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeon-studio/astroplate/HEAD/public/images/call-to-action.png -------------------------------------------------------------------------------- /public/images/logo-darkmode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeon-studio/astroplate/HEAD/public/images/logo-darkmode.png -------------------------------------------------------------------------------- /public/images/image-placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zeon-studio/astroplate/HEAD/public/images/image-placeholder.png -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "dist" 3 | command = "yarn build" 4 | 5 | [build.environment] 6 | NODE_VERSION = "22.20.0" 7 | -------------------------------------------------------------------------------- /src/content/authors/-index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Authors" 3 | meta_title: "" 4 | description: "this is meta description" 5 | image: "" 6 | draft: false 7 | --- 8 | -------------------------------------------------------------------------------- /src/content/blog/-index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Blog Posts" 3 | meta_title: "" 4 | description: "this is meta description" 5 | image: "" 6 | draft: false 7 | --- 8 | -------------------------------------------------------------------------------- /src/content/contact/-index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Contact" 3 | description: "this is meta description" 4 | meta_title: "" 5 | image: "" 6 | draft: false 7 | --- 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "*.mdx": "markdown" 4 | }, 5 | "tailwindCSS.experimental.configFile": "src/styles/main.css" 6 | } 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-astro"], 3 | "overrides": [ 4 | { 5 | "files": ["*.astro"], 6 | "options": { 7 | "parser": "astro" 8 | } 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/layouts/shortcodes/Tab.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | function Tab({ name, children }: { name: string; children: React.ReactNode }) { 4 | return
{children}
; 5 | } 6 | 7 | export default Tab; 8 | -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export type Feature = { 2 | button: button; 3 | image: string; 4 | bulletpoints: string[]; 5 | content: string; 6 | title: string; 7 | }; 8 | 9 | export type Button = { 10 | enable: boolean; 11 | label: string; 12 | link: string; 13 | }; 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | ; https://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = lf 8 | indent_size = 2 9 | indent_style = space 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /src/lib/utils/taxonomyFilter.ts: -------------------------------------------------------------------------------- 1 | import { slugify } from "@/lib/utils/textConverter"; 2 | 3 | const taxonomyFilter = (posts: any[], name: string, key: string) => 4 | posts.filter((post) => 5 | post.data[name].map((name: string) => slugify(name)).includes(key), 6 | ); 7 | 8 | export default taxonomyFilter; 9 | -------------------------------------------------------------------------------- /src/lib/utils/dateFormat.ts: -------------------------------------------------------------------------------- 1 | import { format } from "date-fns"; 2 | 3 | const dateFormat = ( 4 | date: Date | string, 5 | pattern: string = "dd MMM, yyyy", 6 | ): string => { 7 | const dateObj = new Date(date); 8 | const output = format(dateObj, pattern); 9 | return output; 10 | }; 11 | 12 | export default dateFormat; 13 | -------------------------------------------------------------------------------- /.sitepins/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "media": { 3 | "root": "public/images", 4 | "public": "public" 5 | }, 6 | "content": { 7 | "root": "src/content" 8 | }, 9 | "code": { 10 | "root": "src/layouts" 11 | }, 12 | "themeConfig": [ 13 | "src/config" 14 | ], 15 | "arrangement": [], 16 | "showCommitModal": true 17 | } -------------------------------------------------------------------------------- /src/hooks/useTheme.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | const useTheme = (): string => { 4 | const [themeValue, setThemeValue] = useState(""); 5 | 6 | useEffect(() => { 7 | setThemeValue( 8 | document.documentElement.classList.contains("dark") ? "dark" : "light", 9 | ); 10 | }, []); 11 | 12 | return themeValue; 13 | }; 14 | 15 | export default useTheme; 16 | -------------------------------------------------------------------------------- /src/content/sections/call-to-action.md: -------------------------------------------------------------------------------- 1 | --- 2 | enable: true 3 | title: "Ready to build your next project with Astro?" 4 | image: "/images/call-to-action.png" 5 | description: "Experience the future of web development with Astroplate and Astro. Build lightning-fast static sites with ease and flexibility." 6 | button: 7 | enable: true 8 | label: "Get Started Now" 9 | link: "https://github.com/zeon-studio/astroplate" 10 | --- 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | .output/ 4 | .json/ 5 | 6 | # dependencies 7 | node_modules/ 8 | 9 | # logs 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | yarn.lock 15 | package-lock.json 16 | 17 | 18 | # environment variables 19 | .env 20 | .env.production 21 | 22 | # macOS-specific files 23 | .DS_Store 24 | 25 | # ignore .astro directory 26 | .astro 27 | 28 | # ide 29 | .idea/ 30 | -------------------------------------------------------------------------------- /src/layouts/shortcodes/Youtube.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | 3 | const Youtube = ({ 4 | id, 5 | title, 6 | ...rest 7 | }: { 8 | id: string; 9 | title: string; 10 | [key: string]: any; 11 | }) => { 12 | useEffect(() => { 13 | import("@justinribeiro/lite-youtube"); 14 | }, []); 15 | 16 | // @ts-ignore 17 | return ; 18 | }; 19 | 20 | export default Youtube; 21 | -------------------------------------------------------------------------------- /src/styles/safe.css: -------------------------------------------------------------------------------- 1 | /* navbar toggler */ 2 | input#nav-toggle:checked + label #show-button { 3 | @apply hidden; 4 | } 5 | 6 | input#nav-toggle:checked + label #hide-button { 7 | @apply block; 8 | } 9 | 10 | input#nav-toggle:checked ~ #nav-menu { 11 | @apply max-lg:block; 12 | } 13 | 14 | /* swiper pagination */ 15 | .swiper-pagination-bullet { 16 | @apply size-2.5! bg-light! opacity-100! dark:bg-darkmode-light!; 17 | } 18 | 19 | .swiper-pagination-bullet-active { 20 | @apply size-4! bg-primary! dark:bg-darkmode-primary!; 21 | } 22 | -------------------------------------------------------------------------------- /src/layouts/helpers/Disqus.tsx: -------------------------------------------------------------------------------- 1 | import config from "@/config/config.json"; 2 | import { DiscussionEmbed } from "disqus-react"; 3 | import React from "react"; 4 | 5 | const Disqus = ({ className }: { className?: string }) => { 6 | const { disqus } = config; 7 | return ( 8 |
9 | {disqus.enable && ( 10 | 14 | )} 15 |
16 | ); 17 | }; 18 | 19 | export default Disqus; 20 | -------------------------------------------------------------------------------- /src/config/social.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": [ 3 | { 4 | "name": "facebook", 5 | "icon": "FaFacebook", 6 | "link": "https://www.facebook.com/" 7 | }, 8 | { 9 | "name": "x", 10 | "icon": "FaXTwitter", 11 | "link": "https://x.com/" 12 | }, 13 | { 14 | "name": "github", 15 | "icon": "FaGithub", 16 | "link": "https://www.github.com/" 17 | }, 18 | { 19 | "name": "linkedin", 20 | "icon": "FaLinkedin", 21 | "link": "https://www.linkedin.com/" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /src/layouts/partials/PageHeader.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Breadcrumbs from "@/components/Breadcrumbs.astro"; 3 | import { humanize } from "@/lib/utils/textConverter"; 4 | 5 | const { title = "" }: { title?: string } = Astro.props; 6 | --- 7 | 8 |
9 |
10 |
13 |

14 | 15 |

16 |
17 |
18 | -------------------------------------------------------------------------------- /src/styles/main.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @plugin "@tailwindcss/forms"; 3 | @plugin "@tailwindcss/typography"; 4 | @plugin 'tailwind-bootstrap-grid'; 5 | @custom-variant dark (&:where(.dark, .dark *)); 6 | 7 | /* Auto-generated theme from "theme.json"*/ 8 | @import "./generated-theme.css"; 9 | 10 | @import "./safe.css"; 11 | @import "./utilities.css"; 12 | 13 | @layer base { 14 | @import "./base.css"; 15 | } 16 | 17 | @layer components { 18 | @import "./components.css"; 19 | @import "./navigation.css"; 20 | @import "./buttons.css"; 21 | @import "./search.css"; 22 | } 23 | -------------------------------------------------------------------------------- /src/styles/buttons.css: -------------------------------------------------------------------------------- 1 | .btn { 2 | @apply inline-block rounded border border-transparent px-5 py-2 font-semibold capitalize transition; 3 | } 4 | 5 | .btn-sm { 6 | @apply rounded-sm px-4 py-1.5 text-sm; 7 | } 8 | 9 | .btn-primary { 10 | @apply border-primary bg-primary dark:border-darkmode-primary dark:text-text-dark text-white dark:bg-darkmode-primary; 11 | } 12 | 13 | .btn-outline-primary { 14 | @apply border-dark text-text-dark hover:bg-dark dark:hover:text-text-dark bg-transparent hover:text-white dark:border-darkmode-primary dark:text-white dark:hover:bg-darkmode-primary; 15 | } 16 | -------------------------------------------------------------------------------- /src/content/about/-index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Hey, I am John Doe!" 3 | meta_title: "About" 4 | description: "this is meta description" 5 | image: "/images/avatar.png" 6 | draft: false 7 | --- 8 | 9 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Corporis illum nesciunt commodi vel nisi ut alias excepturi ipsum, totam, labore tempora, odit ex iste tempore sed. Fugit voluptatibus perspiciatis assumenda nulla ad nihil, omnis vel, doloremque sit quam autem optio maiores, illum eius facilis et quo consectetur provident dolor similique! Enim voluptatem dicta expedita veritatis repellat dolorum impedit, provident quasi at. 10 | -------------------------------------------------------------------------------- /src/content/authors/john-doe.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: John Doe 3 | email: johndoe@email.com 4 | image: "/images/avatar.png" 5 | description: this is meta description 6 | social: 7 | - name: github 8 | icon: FaGithub 9 | link: https://github.com 10 | 11 | - name: twitter 12 | icon: FaTwitter 13 | link: https://twitter.com 14 | 15 | - name: linkedin 16 | icon: FaLinkedin 17 | link: https://linkedin.com 18 | --- 19 | 20 | lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostr navigation et dolore magna aliqua. 21 | -------------------------------------------------------------------------------- /src/content/authors/sam-wilson.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Sam Wilson 3 | email: samwilson@email.com 4 | image: "/images/avatar.png" 5 | description: this is meta description 6 | social: 7 | - name: github 8 | icon: FaGithub 9 | link: https://github.com 10 | 11 | - name: twitter 12 | icon: FaTwitter 13 | link: https://twitter.com 14 | 15 | - name: linkedin 16 | icon: FaLinkedin 17 | link: https://linkedin.com 18 | --- 19 | 20 | lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostr navigation et dolore magna aliqua. 21 | -------------------------------------------------------------------------------- /src/content/authors/william-jacob.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: William Jacob 3 | email: williamjacob@email.com 4 | image: "/images/avatar.png" 5 | description: this is meta description 6 | social: 7 | - name: github 8 | icon: FaGithub 9 | link: https://github.com 10 | 11 | - name: twitter 12 | icon: FaTwitter 13 | link: https://twitter.com 14 | 15 | - name: linkedin 16 | icon: FaLinkedin 17 | link: https://linkedin.com 18 | --- 19 | 20 | lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostr navigation et dolore magna aliqua. 21 | -------------------------------------------------------------------------------- /src/styles/utilities.css: -------------------------------------------------------------------------------- 1 | @utility bg-gradient { 2 | @apply bg-linear-to-b from-[rgba(249,249,249,1)] from-[0.53%] to-white to-[83.28%] dark:from-darkmode-light dark:to-darkmode-body; 3 | } 4 | 5 | /* form style */ 6 | @utility form-input { 7 | @apply w-full rounded border-transparent bg-light px-6 py-4 text-text-dark placeholder:text-text-light focus:border-primary dark:focus:border-darkmode-primary focus:ring-transparent dark:border-darkmode-border dark:bg-darkmode-light dark:text-darkmode-text-light; 8 | } 9 | 10 | @utility form-label { 11 | @apply mb-4 block font-secondary text-xl font-normal text-text-dark dark:text-darkmode-text-light; 12 | } 13 | -------------------------------------------------------------------------------- /src/layouts/shortcodes/Video.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | function Video({ 3 | title, 4 | width = 500, 5 | height = "auto", 6 | src, 7 | ...rest 8 | }: { 9 | title: string; 10 | width: number; 11 | height: number | "auto"; 12 | src: string; 13 | [key: string]: any; 14 | }) { 15 | return ( 16 | 29 | ); 30 | } 31 | 32 | export default Video; 33 | -------------------------------------------------------------------------------- /src/layouts/components/TwSizeIndicator.astro: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | { 5 | process.env.NODE_ENV === "development" && ( 6 |
7 | all 8 | 9 | 10 | 11 | 12 | 13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /src/layouts/components/Social.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const { source, className } = Astro.props; 3 | import DynamicIcon from "@/helpers/DynamicIcon"; 4 | 5 | export interface ISocial { 6 | name: string; 7 | icon: string; 8 | link: string; 9 | } 10 | --- 11 | 12 | 29 | -------------------------------------------------------------------------------- /src/layouts/shortcodes/Button.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Button = ({ 4 | label, 5 | link, 6 | style, 7 | rel, 8 | }: { 9 | label: string; 10 | link: string; 11 | style?: string; 12 | rel?: string; 13 | }) => { 14 | return ( 15 | 25 | {label} 26 | 27 | ); 28 | }; 29 | 30 | export default Button; 31 | -------------------------------------------------------------------------------- /src/pages/blog/[single].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Base from "@/layouts/Base.astro"; 3 | import PostSingle from "@/layouts/PostSingle.astro"; 4 | import { getSinglePage } from "@/lib/contentParser.astro"; 5 | 6 | export async function getStaticPaths() { 7 | const BLOG_FOLDER = "blog"; 8 | const posts = await getSinglePage(BLOG_FOLDER); 9 | 10 | const paths = posts.map((post) => ({ 11 | params: { 12 | single: post.id, 13 | }, 14 | props: { post }, 15 | })); 16 | return paths; 17 | } 18 | 19 | const { post } = Astro.props; 20 | const { title, meta_title, description, image } = post.data; 21 | --- 22 | 23 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "target": "es6", 6 | "allowJs": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "jsx": "react", 11 | "isolatedModules": true, 12 | "incremental": true, 13 | "allowSyntheticDefaultImports": true, 14 | "paths": { 15 | "@/components/*": ["./src/layouts/components/*"], 16 | "@/shortcodes/*": ["./src/layouts/shortcodes/*"], 17 | "@/helpers/*": ["./src/layouts/helpers/*"], 18 | "@/partials/*": ["./src/layouts/partials/*"], 19 | "@/*": ["./src/*"] 20 | } 21 | }, 22 | "include": [".astro/types.d.ts", "**/*.ts", "**/*.tsx", "**/*.astro"], 23 | "exclude": ["node_modules", "dist"] 24 | } 25 | -------------------------------------------------------------------------------- /config/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes 1; 2 | 3 | events { 4 | worker_connections 1024; 5 | } 6 | 7 | http { 8 | server { 9 | listen 80; 10 | server_name _; 11 | 12 | root /usr/share/nginx/html; 13 | index index.html index.htm; 14 | include /etc/nginx/mime.types; 15 | 16 | gzip on; 17 | gzip_min_length 1000; 18 | gzip_proxied expired no-cache no-store private auth; 19 | gzip_types text/plain text/css application/json application/javascript application/x-javascript text/xml application/xml application/xml+rss text/javascript; 20 | 21 | error_page 404 /404.html; 22 | location = /404.html { 23 | root /usr/share/nginx/html; 24 | internal; 25 | } 26 | 27 | location / { 28 | try_files $uri ${uri}.html $uri/index.html =404; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/layouts/components/AuthorCard.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { plainify } from "@/lib/utils/textConverter"; 3 | import ImageMod from "./ImageMod.astro"; 4 | import Social from "./Social.astro"; 5 | 6 | const { data } = Astro.props; 7 | const { title, image, social } = data.data; 8 | --- 9 | 10 |
11 | { 12 | image && ( 13 | 21 | ) 22 | } 23 |

24 | {title} 25 |

26 |

27 | {plainify(data.body?.slice(0, 100))} 28 |

29 | 30 |
31 | -------------------------------------------------------------------------------- /src/pages/404.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Base from "@/layouts/Base.astro"; 3 | --- 4 | 5 | 6 |
7 |
8 |
9 |
10 | 13 | 404 14 | 15 |

Page not found

16 |
17 |

18 | The page you are looking for might have been removed, had its name 19 | changed, or is temporarily unavailable. 20 |

21 |
22 | Back to home 23 |
24 |
25 |
26 |
27 | 28 | -------------------------------------------------------------------------------- /src/lib/utils/sortFunctions.ts: -------------------------------------------------------------------------------- 1 | // sort by date 2 | export const sortByDate = (array: any[]) => { 3 | const sortedArray = array.sort( 4 | (a: any, b: any) => 5 | new Date(b.data.date && b.data.date).valueOf() - 6 | new Date(a.data.date && a.data.date).valueOf(), 7 | ); 8 | return sortedArray; 9 | }; 10 | 11 | // sort product by weight 12 | export const sortByWeight = (array: any[]) => { 13 | const withWeight = array.filter( 14 | (item: { data: { weight: any } }) => item.data.weight, 15 | ); 16 | const withoutWeight = array.filter( 17 | (item: { data: { weight: any } }) => !item.data.weight, 18 | ); 19 | const sortedWeightedArray = withWeight.sort( 20 | (a: { data: { weight: number } }, b: { data: { weight: number } }) => 21 | a.data.weight - b.data.weight, 22 | ); 23 | const sortedArray = [...new Set([...sortedWeightedArray, ...withoutWeight])]; 24 | return sortedArray; 25 | }; 26 | -------------------------------------------------------------------------------- /src/pages/authors/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import AuthorCard from "@/components/AuthorCard.astro"; 3 | import Base from "@/layouts/Base.astro"; 4 | import { getListPage, getSinglePage } from "@/lib/contentParser.astro"; 5 | import PageHeader from "@/partials/PageHeader.astro"; 6 | 7 | const authorIndex = await getListPage("authors", "-index"); 8 | 9 | if (authorIndex.data.draft) { 10 | return Astro.redirect("/404"); 11 | } 12 | 13 | const authors = await getSinglePage("authors"); 14 | --- 15 | 16 | 17 | 18 |
19 |
20 |
21 | { 22 | authors.map((author) => ( 23 |
24 | 25 |
26 | )) 27 | } 28 |
29 |
30 |
31 | 32 | -------------------------------------------------------------------------------- /src/styles/base.css: -------------------------------------------------------------------------------- 1 | html { 2 | @apply text-base-sm md:text-base; 3 | } 4 | 5 | body { 6 | @apply bg-body text-base dark:bg-darkmode-body font-primary font-normal leading-relaxed text-text dark:text-darkmode-text; 7 | } 8 | 9 | h1, 10 | h2, 11 | h3, 12 | h4, 13 | h5, 14 | h6 { 15 | @apply font-secondary font-bold leading-tight text-text-dark dark:text-darkmode-text-dark; 16 | } 17 | 18 | h1, 19 | .h1 { 20 | @apply text-h1-sm md:text-h1; 21 | } 22 | 23 | h2, 24 | .h2 { 25 | @apply text-h2-sm md:text-h2; 26 | } 27 | 28 | h3, 29 | .h3 { 30 | @apply text-h3-sm md:text-h3; 31 | } 32 | 33 | h4, 34 | .h4 { 35 | @apply text-h4; 36 | } 37 | 38 | h5, 39 | .h5 { 40 | @apply text-h5; 41 | } 42 | 43 | h6, 44 | .h6 { 45 | @apply text-h6; 46 | } 47 | 48 | b, 49 | strong { 50 | @apply font-semibold; 51 | } 52 | 53 | code { 54 | @apply after:border-none; 55 | } 56 | 57 | blockquote > p { 58 | @apply my-0!; 59 | } 60 | 61 | button { 62 | @apply cursor-pointer; 63 | } 64 | -------------------------------------------------------------------------------- /src/lib/utils/readingTime.ts: -------------------------------------------------------------------------------- 1 | // content reading 2 | const readingTime = (content: string): string => { 3 | const WPS = 275 / 60; 4 | 5 | let images = 0; 6 | const regex = /\w/; 7 | 8 | let words = content.split(" ").filter((word) => { 9 | if (word.includes(" 3) { 22 | imageFactor -= 1; 23 | } 24 | images -= 1; 25 | } 26 | 27 | const minutes = Math.ceil(((words - imageAdjust) / WPS + imageSecs) / 60); 28 | 29 | if (minutes < 10) { 30 | if (minutes < 2) { 31 | return "0" + minutes + ` Min read`; 32 | } else { 33 | return "0" + minutes + ` Mins read`; 34 | } 35 | } else { 36 | return minutes + ` Mins read`; 37 | } 38 | }; 39 | 40 | export default readingTime; 41 | -------------------------------------------------------------------------------- /src/config/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors": { 3 | "default": { 4 | "theme_color": { 5 | "primary": "#121212", 6 | "body": "#fff", 7 | "border": "#eaeaea", 8 | "light": "#f6f6f6", 9 | "dark": "#040404" 10 | }, 11 | "text_color": { 12 | "text": "#444444", 13 | "text_dark": "#040404", 14 | "text_light": "#717171" 15 | } 16 | }, 17 | "darkmode": { 18 | "theme_color": { 19 | "primary": "#fff", 20 | "body": "#1c1c1c", 21 | "border": "#3E3E3E", 22 | "light": "#222222", 23 | "dark": "#fff" 24 | }, 25 | "text_color": { 26 | "text": "#B4AFB6", 27 | "text_dark": "#fff", 28 | "text_light": "#B4AFB6" 29 | } 30 | } 31 | }, 32 | "fonts": { 33 | "font_family": { 34 | "primary": "Heebo:wght@400;600", 35 | "primary_type": "sans-serif", 36 | "secondary": "Signika:wght@500;700", 37 | "secondary_type": "sans-serif" 38 | }, 39 | "font_size": { 40 | "base": "16", 41 | "scale": "1.2" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/lib/utils/similarItems.ts: -------------------------------------------------------------------------------- 1 | // similar products 2 | const similarItems = (currentItem: any, allItems: any[]) => { 3 | let categories: string[] = []; 4 | let tags: string[] = []; 5 | 6 | // set categories 7 | if (currentItem.data.categories.length > 0) { 8 | categories = currentItem.data.categories; 9 | } 10 | 11 | // set tags 12 | if (currentItem.data.tags.length > 0) { 13 | tags = currentItem.data.tags; 14 | } 15 | 16 | // filter by categories 17 | const filterByCategories = allItems.filter((item: any) => 18 | categories.find((category) => item.data.categories.includes(category)), 19 | ); 20 | 21 | // filter by tags 22 | const filterByTags = allItems.filter((item: any) => 23 | tags.find((tag) => item.data.tags.includes(tag)), 24 | ); 25 | 26 | // merged after filter 27 | const mergedItems = [...new Set([...filterByCategories, ...filterByTags])]; 28 | 29 | // filter by slug 30 | const filterBySlug = mergedItems.filter( 31 | (product) => product.id !== currentItem.id, 32 | ); 33 | 34 | return filterBySlug; 35 | }; 36 | 37 | export default similarItems; 38 | -------------------------------------------------------------------------------- /src/lib/contentParser.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { 3 | getCollection, 4 | getEntry, 5 | type CollectionEntry, 6 | type CollectionKey, 7 | } from "astro:content"; 8 | 9 | type PageData = { 10 | title: string; 11 | meta_title?: string; 12 | description?: string; 13 | image?: string; 14 | draft?: boolean; 15 | }; 16 | 17 | export const getSinglePage = async ( 18 | collectionName: C 19 | ): Promise[]> => { 20 | const allPages = await getCollection( 21 | collectionName, 22 | ({ data, id }) => !(data as PageData)?.draft && !id.startsWith("-") 23 | ); 24 | return allPages; 25 | }; 26 | 27 | export const getListPage = async ( 28 | collectionName: C, 29 | documentId: "-index" | string 30 | ): Promise> => { 31 | const data = (await getEntry( 32 | collectionName, 33 | documentId 34 | )) as CollectionEntry | null; 35 | 36 | if (!data) { 37 | throw new Error( 38 | `No page found for the collection: ${collectionName} with filename: ${documentId}` 39 | ); 40 | } 41 | 42 | return data; 43 | }; 44 | --- 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 - Present, Zeon Studio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/config/menu.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": [ 3 | { 4 | "name": "Home", 5 | "url": "/" 6 | }, 7 | { 8 | "name": "About", 9 | "url": "/about" 10 | }, 11 | { 12 | "name": "Elements", 13 | "url": "/elements" 14 | }, 15 | { 16 | "name": "Pages", 17 | "url": "", 18 | "hasChildren": true, 19 | "children": [ 20 | { 21 | "name": "Contact", 22 | "url": "/contact" 23 | }, 24 | { 25 | "name": "Blog", 26 | "url": "/blog" 27 | }, 28 | { 29 | "name": "Authors", 30 | "url": "/authors" 31 | }, 32 | { 33 | "name": "Categories", 34 | "url": "/categories" 35 | }, 36 | { 37 | "name": "Tags", 38 | "url": "/tags" 39 | }, 40 | { 41 | "name": "404 Page", 42 | "url": "/404" 43 | } 44 | ] 45 | } 46 | ], 47 | "footer": [ 48 | { 49 | "name": "Elements", 50 | "url": "/elements" 51 | }, 52 | { 53 | "name": "Privacy Policy", 54 | "url": "/privacy-policy" 55 | } 56 | ] 57 | } 58 | -------------------------------------------------------------------------------- /src/layouts/shortcodes/Accordion.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | const Accordion = ({ 4 | title, 5 | children, 6 | className, 7 | }: { 8 | title: string; 9 | children: React.ReactNode; 10 | className?: string; 11 | }) => { 12 | const [show, setShow] = useState(false); 13 | 14 | return ( 15 |
16 | 31 |
{children}
32 |
33 | ); 34 | }; 35 | 36 | export default Accordion; 37 | -------------------------------------------------------------------------------- /src/pages/[regular].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Base from "@/layouts/Base.astro"; 3 | import { getSinglePage } from "@/lib/contentParser.astro"; 4 | import PageHeader from "@/partials/PageHeader.astro"; 5 | import { render } from "astro:content"; 6 | 7 | // get static paths for all pages 8 | export async function getStaticPaths() { 9 | const COLLECTION_FOLDER = "pages"; 10 | 11 | const pages = await getSinglePage(COLLECTION_FOLDER); 12 | 13 | const paths = pages.map((page) => ({ 14 | params: { 15 | regular: page.id, 16 | }, 17 | props: { page }, 18 | })); 19 | return paths; 20 | } 21 | 22 | const { page } = Astro.props; 23 | const { title, meta_title, description, image } = page.data; 24 | const { Content } = await render(page); 25 | --- 26 | 27 | 33 | 34 |
35 |
36 |
37 |
38 |
39 | 40 |
41 |
42 |
43 |
44 |
45 | 46 | -------------------------------------------------------------------------------- /.sitepins/schema/authors.json: -------------------------------------------------------------------------------- 1 | { 2 | "file": "src/content/authors/john-doe.md", 3 | "name": "authors", 4 | "fileType": "md", 5 | "fmType": "yaml", 6 | "template": [ 7 | { 8 | "label": "Title", 9 | "name": "title", 10 | "type": "string", 11 | "value": "" 12 | }, 13 | { 14 | "label": "Email", 15 | "name": "email", 16 | "type": "string", 17 | "value": "" 18 | }, 19 | { 20 | "label": "Image", 21 | "name": "image", 22 | "type": "media", 23 | "value": "" 24 | }, 25 | { 26 | "label": "Description", 27 | "name": "description", 28 | "type": "string", 29 | "value": "" 30 | }, 31 | { 32 | "label": "Social", 33 | "name": "social", 34 | "type": "Array", 35 | "fields": [ 36 | { 37 | "label": "Name", 38 | "name": "name", 39 | "type": "string", 40 | "value": "" 41 | }, 42 | { 43 | "label": "Icon", 44 | "name": "icon", 45 | "type": "string", 46 | "value": "" 47 | }, 48 | { 49 | "label": "Link", 50 | "name": "link", 51 | "type": "string", 52 | "value": "" 53 | } 54 | ] 55 | } 56 | ] 57 | } -------------------------------------------------------------------------------- /src/lib/taxonomyParser.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getSinglePage } from "@/lib/contentParser.astro"; 3 | import { slugify } from "@/lib/utils/textConverter"; 4 | 5 | // get taxonomy from frontmatter 6 | export const getTaxonomy = async (collection: any, name: string) => { 7 | const singlePages = await getSinglePage(collection); 8 | const taxonomyPages = singlePages.map((page: any) => page.data[name]); 9 | let taxonomies: string[] = []; 10 | for (let i = 0; i < taxonomyPages.length; i++) { 11 | const categoryArray = taxonomyPages[i]; 12 | for (let j = 0; j < categoryArray.length; j++) { 13 | taxonomies.push(slugify(categoryArray[j])); 14 | } 15 | } 16 | const taxonomy = [...new Set(taxonomies)]; 17 | return taxonomy; 18 | }; 19 | 20 | // get all taxonomies from frontmatter 21 | export const getAllTaxonomy = async (collection: any, name: string) => { 22 | const singlePages = await getSinglePage(collection); 23 | const taxonomyPages = singlePages.map((page: any) => page.data[name]); 24 | let taxonomies: string[] = []; 25 | for (let i = 0; i < taxonomyPages.length; i++) { 26 | const categoryArray = taxonomyPages[i]; 27 | for (let j = 0; j < categoryArray.length; j++) { 28 | taxonomies.push(slugify(categoryArray[j])); 29 | } 30 | } 31 | return taxonomies; 32 | }; 33 | --- 34 | -------------------------------------------------------------------------------- /src/lib/utils/bgImageMod.ts: -------------------------------------------------------------------------------- 1 | import { getImage } from "astro:assets"; 2 | 3 | const bgImageMod = async ( 4 | src: string, 5 | format?: "auto" | "avif" | "jpeg" | "png" | "svg" | "webp", 6 | ) => { 7 | src = `/public${src}`; 8 | const images = import.meta.glob("/public/images/**/*.{jpeg,jpg,png,gif}"); 9 | 10 | // Check if the source path is valid 11 | if (!src || !images[src]) { 12 | console.error( 13 | `\x1b[31mImage not found - ${src}.\x1b[0m Make sure the image is in the /public/images folder.`, 14 | ); 15 | 16 | return ""; // Return an empty string if the image is not found 17 | } 18 | 19 | // Function to get the image info like width, height, format, etc. 20 | const getImagePath = async (image: string) => { 21 | try { 22 | const imageData = (await images[image]()) as any; 23 | return imageData; 24 | } catch (error) { 25 | return `Image not found - ${src}. Make sure the image is in the /public/images folder.`; 26 | } 27 | }; 28 | 29 | // Get the image data for the specified source path 30 | const image = await getImagePath(src); 31 | 32 | // Optimize the image for development 33 | const ImageMod = await getImage({ 34 | src: image.default, 35 | format: format, 36 | }); 37 | 38 | return ImageMod.src; 39 | }; 40 | 41 | export default bgImageMod; 42 | -------------------------------------------------------------------------------- /src/layouts/helpers/DynamicIcon.tsx: -------------------------------------------------------------------------------- 1 | import React, { type FC } from "react"; 2 | import type { IconType } from "react-icons"; 3 | import * as FaIcons from "react-icons/fa6"; 4 | // import * as AiIcons from "react-icons/ai"; 5 | // import * as BsIcons from "react-icons/bs"; 6 | // import * as FiIcons from "react-icons/fi"; 7 | // import * as Io5Icons from "react-icons/io5"; 8 | // import * as RiIcons from "react-icons/ri"; 9 | // import * as TbIcons from "react-icons/tb"; 10 | // import * as TfiIcons from "react-icons/tfi"; 11 | 12 | type IconMap = Record; 13 | 14 | interface IDynamicIcon extends React.SVGProps { 15 | icon: string; 16 | className?: string; 17 | } 18 | 19 | const iconLibraries: { [key: string]: IconMap } = { 20 | fa: FaIcons, 21 | }; 22 | 23 | const DynamicIcon: FC = ({ icon, ...props }) => { 24 | const IconLibrary = getIconLibrary(icon); 25 | const Icon = IconLibrary ? IconLibrary[icon] : undefined; 26 | 27 | if (!Icon) { 28 | return Icon not found; 29 | } 30 | 31 | return ; 32 | }; 33 | 34 | const getIconLibrary = (icon: string): IconMap | undefined => { 35 | const libraryKey = icon.substring(0, 2).toLowerCase(); 36 | 37 | return iconLibraries[libraryKey]; 38 | }; 39 | 40 | export default DynamicIcon; 41 | -------------------------------------------------------------------------------- /src/pages/tags/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Base from "@/layouts/Base.astro"; 3 | import { getAllTaxonomy, getTaxonomy } from "@/lib/taxonomyParser.astro"; 4 | import { humanize } from "@/lib/utils/textConverter"; 5 | import PageHeader from "@/partials/PageHeader.astro"; 6 | 7 | const BLOG_FOLDER = "blog"; 8 | 9 | const tags = await getTaxonomy(BLOG_FOLDER, "tags"); 10 | const allTags = await getAllTaxonomy(BLOG_FOLDER, "tags"); 11 | --- 12 | 13 | 14 | 15 |
16 |
17 | 37 |
38 |
39 | 40 | -------------------------------------------------------------------------------- /src/pages/tags/[tag].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import BlogCard from "@/components/BlogCard.astro"; 3 | import Base from "@/layouts/Base.astro"; 4 | import { getSinglePage } from "@/lib/contentParser.astro"; 5 | import { getTaxonomy } from "@/lib/taxonomyParser.astro"; 6 | import { sortByDate } from "@/lib/utils/sortFunctions"; 7 | import taxonomyFilter from "@/lib/utils/taxonomyFilter"; 8 | import PageHeader from "@/partials/PageHeader.astro"; 9 | 10 | export async function getStaticPaths() { 11 | const BLOG_FOLDER = "blog"; 12 | const tags = await getTaxonomy(BLOG_FOLDER, "tags"); 13 | 14 | return tags.map((tag) => { 15 | return { 16 | params: { tag }, 17 | }; 18 | }); 19 | } 20 | 21 | const { tag } = Astro.params; 22 | 23 | // get posts by tag 24 | const BLOG_FOLDER = "blog"; 25 | const posts = await getSinglePage(BLOG_FOLDER); 26 | const filterByTags = taxonomyFilter(posts, "tags", tag!); 27 | const sortedPosts = sortByDate(filterByTags); 28 | --- 29 | 30 | 31 | 32 |
33 |
34 |
35 | { 36 | sortedPosts.map((post) => ( 37 |
38 | 39 |
40 | )) 41 | } 42 |
43 |
44 |
45 | 46 | -------------------------------------------------------------------------------- /src/styles/generated-theme.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Auto-generated from "src/config/theme.json" 3 | * DO NOT EDIT THIS FILE MANUALLY 4 | * Run: node scripts/themeGenerator.js 5 | */ 6 | 7 | @theme { 8 | /* === Colors === */ 9 | --color-primary: #121212; 10 | --color-body: #fff; 11 | --color-border: #eaeaea; 12 | --color-light: #f6f6f6; 13 | --color-dark: #040404; 14 | --color-text: #444444; 15 | --color-text-dark: #040404; 16 | --color-text-light: #717171; 17 | 18 | /* === Darkmode Colors === */ 19 | --color-darkmode-primary: #fff; 20 | --color-darkmode-body: #1c1c1c; 21 | --color-darkmode-border: #3E3E3E; 22 | --color-darkmode-light: #222222; 23 | --color-darkmode-dark: #fff; 24 | --color-darkmode-text: #B4AFB6; 25 | --color-darkmode-text-dark: #fff; 26 | --color-darkmode-text-light: #B4AFB6; 27 | 28 | /* === Font Families === */ 29 | --font-primary: Heebo, sans-serif; 30 | --font-secondary: Signika, sans-serif; 31 | 32 | /* === Font Sizes === */ 33 | --text-base: 16px; 34 | --text-base-sm: 12.8px; 35 | --text-h6: 1.2rem; 36 | --text-h6-sm: 1.08rem; 37 | --text-h5: 1.44rem; 38 | --text-h5-sm: 1.296rem; 39 | --text-h4: 1.728rem; 40 | --text-h4-sm: 1.5552rem; 41 | --text-h3: 2.0736rem; 42 | --text-h3-sm: 1.86624rem; 43 | --text-h2: 2.48832rem; 44 | --text-h2-sm: 2.239488rem; 45 | --text-h1: 2.9859839999999997rem; 46 | --text-h1-sm: 2.6873856rem; 47 | } 48 | -------------------------------------------------------------------------------- /astro.config.mjs: -------------------------------------------------------------------------------- 1 | import mdx from "@astrojs/mdx"; 2 | import react from "@astrojs/react"; 3 | import sitemap from "@astrojs/sitemap"; 4 | import tailwindcss from "@tailwindcss/vite"; 5 | import AutoImport from "astro-auto-import"; 6 | import { defineConfig } from "astro/config"; 7 | import remarkCollapse from "remark-collapse"; 8 | import remarkToc from "remark-toc"; 9 | import sharp from "sharp"; 10 | import config from "./src/config/config.json"; 11 | 12 | // https://astro.build/config 13 | export default defineConfig({ 14 | site: config.site.base_url ? config.site.base_url : "http://examplesite.com", 15 | base: config.site.base_path ? config.site.base_path : "/", 16 | trailingSlash: config.site.trailing_slash ? "always" : "never", 17 | image: { service: sharp() }, 18 | vite: { plugins: [tailwindcss()] }, 19 | integrations: [ 20 | react(), 21 | sitemap(), 22 | AutoImport({ 23 | imports: [ 24 | "@/shortcodes/Button", 25 | "@/shortcodes/Accordion", 26 | "@/shortcodes/Notice", 27 | "@/shortcodes/Video", 28 | "@/shortcodes/Youtube", 29 | "@/shortcodes/Tabs", 30 | "@/shortcodes/Tab", 31 | ], 32 | }), 33 | mdx(), 34 | ], 35 | markdown: { 36 | remarkPlugins: [remarkToc, [remarkCollapse, { test: "Table of contents" }]], 37 | shikiConfig: { theme: "one-dark-pro", wrap: true }, 38 | extendDefaultPlugins: true, 39 | }, 40 | }); 41 | -------------------------------------------------------------------------------- /src/pages/about.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import ImageMod from "@/components/ImageMod.astro"; 3 | import Base from "@/layouts/Base.astro"; 4 | import { getListPage } from "@/lib/contentParser.astro"; 5 | import { markdownify } from "@/lib/utils/textConverter"; 6 | import { render } from "astro:content"; 7 | 8 | const aboutIndex = await getListPage("about", "-index"); 9 | const { Content } = await render(aboutIndex); 10 | const { title, description, meta_title, image } = aboutIndex.data; 11 | 12 | if (aboutIndex.data.draft) { 13 | return Astro.redirect("/404"); 14 | } 15 | --- 16 | 17 | 23 |
24 |
25 |
26 |
27 | { 28 | image && ( 29 | 37 | ) 38 | } 39 |

40 |
41 | 42 |
43 |

44 |
45 |
46 |
47 | 48 | -------------------------------------------------------------------------------- /src/layouts/components/Breadcrumbs.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { humanize } from "@/lib/utils/textConverter"; 3 | 4 | const { className }: { className?: string } = Astro.props; 5 | 6 | const paths = Astro.url.pathname.split("/").filter((x) => x); 7 | let parts = [ 8 | { 9 | label: "Home", 10 | href: "/", 11 | "aria-label": Astro.url.pathname === "/" ? "page" : undefined, 12 | }, 13 | ]; 14 | 15 | paths.forEach((label: string, i: number) => { 16 | const href = `/${paths.slice(0, i + 1).join("/")}`; 17 | label !== "page" && 18 | parts.push({ 19 | label: humanize(label.replace(".html", "").replace(/[-_]/g, " ")) || "", 20 | href, 21 | "aria-label": Astro.url.pathname === href ? "page" : undefined, 22 | }); 23 | }); 24 | --- 25 | 26 | 46 | -------------------------------------------------------------------------------- /src/pages/categories/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Base from "@/layouts/Base.astro"; 3 | import { getAllTaxonomy, getTaxonomy } from "@/lib/taxonomyParser.astro"; 4 | import { humanize } from "@/lib/utils/textConverter"; 5 | import PageHeader from "@/partials/PageHeader.astro"; 6 | 7 | const BLOG_FOLDER = "blog"; 8 | 9 | const categories = await getTaxonomy(BLOG_FOLDER, "categories"); 10 | const allCategories = await getAllTaxonomy(BLOG_FOLDER, "categories"); 11 | --- 12 | 13 | 14 | 15 |
16 |
17 | 37 |
38 |
39 | 40 | -------------------------------------------------------------------------------- /src/content/blog/post-3.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How to build an Application with modern Technology" 3 | meta_title: "" 4 | description: "this is meta description" 5 | date: 2022-04-04T05:00:00Z 6 | image: "/images/image-placeholder.png" 7 | categories: ["Software"] 8 | author: "John Doe" 9 | tags: ["software", "tailwind"] 10 | draft: false 11 | --- 12 | 13 | Nemo vel ad consectetur namut rutrum ex, venenatis sollicitudin urna. Aliquam erat volutpat. Integer eu ipsum sem. Ut bibendum lacus vestibulum maximus suscipit. Quisque vitae nibh iaculis neque blandit euismod. 14 | 15 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius! 16 | 17 | ## Creative Design 18 | 19 | Nam ut rutrum ex, venenatis sollicitudin urna. Aliquam erat volutpat. Integer eu ipsum sem. Ut bibendum lacus vestibulum maximus suscipit. Quisque vitae nibh iaculis neque blandit euismod. 20 | 21 | > Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius! 22 | 23 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius! 24 | -------------------------------------------------------------------------------- /src/layouts/partials/Footer.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Logo from "@/components/Logo.astro"; 3 | import Social from "@/components/Social.astro"; 4 | import config from "@/config/config.json"; 5 | import menu from "@/config/menu.json"; 6 | import social from "@/config/social.json"; 7 | import { markdownify } from "@/lib/utils/textConverter"; 8 | 9 | const { footer }: { footer: { name: string; url: string }[] } = menu; 10 | --- 11 | 12 |
13 |
14 |
15 |
16 | 17 |
18 |
19 |
    20 | { 21 | footer.map((menu) => ( 22 |
  • 23 | {menu.name} 24 |
  • 25 | )) 26 | } 27 |
28 |
29 |
30 | 31 |
32 |
33 |
34 |
35 |
38 |

39 |

40 |
41 |
42 | -------------------------------------------------------------------------------- /src/content/blog/post-4.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How to build an Application with modern Technology" 3 | meta_title: "" 4 | description: "this is meta description" 5 | date: 2022-04-04T05:00:00Z 6 | image: "/images/image-placeholder.png" 7 | categories: ["Architecture"] 8 | author: "John Doe" 9 | tags: ["silicon", "technology"] 10 | draft: false 11 | --- 12 | 13 | Nemo vel ad consectetur namut rutrum ex, venenatis sollicitudin urna. Aliquam erat volutpat. Integer eu ipsum sem. Ut bibendum lacus vestibulum maximus suscipit. Quisque vitae nibh iaculis neque blandit euismod. 14 | 15 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius! 16 | 17 | ## Creative Design 18 | 19 | Nam ut rutrum ex, venenatis sollicitudin urna. Aliquam erat volutpat. Integer eu ipsum sem. Ut bibendum lacus vestibulum maximus suscipit. Quisque vitae nibh iaculis neque blandit euismod. 20 | 21 | > Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius! 22 | 23 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius! 24 | -------------------------------------------------------------------------------- /src/content/blog/post-1.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How to build an Application with modern Technology" 3 | meta_title: "" 4 | description: "this is meta description" 5 | date: 2022-04-04T05:00:00Z 6 | image: "/images/image-placeholder.png" 7 | categories: ["Application", "Data"] 8 | author: "John Doe" 9 | tags: ["nextjs", "tailwind"] 10 | draft: false 11 | --- 12 | 13 | Nemo vel ad consectetur namut rutrum ex, venenatis sollicitudin urna. Aliquam erat volutpat. Integer eu ipsum sem. Ut bibendum lacus vestibulum maximus suscipit. Quisque vitae nibh iaculis neque blandit euismod. 14 | 15 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius! 16 | 17 | ## Creative Design 18 | 19 | Nam ut rutrum ex, venenatis sollicitudin urna. Aliquam erat volutpat. Integer eu ipsum sem. Ut bibendum lacus vestibulum maximus suscipit. Quisque vitae nibh iaculis neque blandit euismod. 20 | 21 | > Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius! 22 | 23 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius! 24 | -------------------------------------------------------------------------------- /src/content/blog/post-2.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "How to build an Application with modern Technology" 3 | meta_title: "" 4 | description: "this is meta description" 5 | date: 2022-04-04T05:00:00Z 6 | image: "/images/image-placeholder.png" 7 | categories: ["Technology", "Data"] 8 | author: "Sam Wilson" 9 | tags: ["technology", "tailwind"] 10 | draft: false 11 | --- 12 | 13 | Nemo vel ad consectetur namut rutrum ex, venenatis sollicitudin urna. Aliquam erat volutpat. Integer eu ipsum sem. Ut bibendum lacus vestibulum maximus suscipit. Quisque vitae nibh iaculis neque blandit euismod. 14 | 15 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius! 16 | 17 | ## Creative Design 18 | 19 | Nam ut rutrum ex, venenatis sollicitudin urna. Aliquam erat volutpat. Integer eu ipsum sem. Ut bibendum lacus vestibulum maximus suscipit. Quisque vitae nibh iaculis neque blandit euismod. 20 | 21 | > Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius! 22 | 23 | Lorem ipsum dolor sit amet consectetur adipisicing elit. Nemo vel ad consectetur ut aperiam. Itaque eligendi natus aperiam? Excepturi repellendus consequatur quibusdam optio expedita praesentium est adipisci dolorem ut eius! 24 | -------------------------------------------------------------------------------- /src/pages/categories/[category].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import BlogCard from "@/components/BlogCard.astro"; 3 | import Base from "@/layouts/Base.astro"; 4 | import { getSinglePage } from "@/lib/contentParser.astro"; 5 | import { getTaxonomy } from "@/lib/taxonomyParser.astro"; 6 | import { sortByDate } from "@/lib/utils/sortFunctions"; 7 | import taxonomyFilter from "@/lib/utils/taxonomyFilter"; 8 | import PageHeader from "@/partials/PageHeader.astro"; 9 | 10 | // get static paths for all categories 11 | export async function getStaticPaths() { 12 | const BLOG_FOLDER = "blog"; 13 | const categories = await getTaxonomy(BLOG_FOLDER, "categories"); 14 | 15 | return categories.map((category) => { 16 | return { 17 | params: { category }, 18 | }; 19 | }); 20 | } 21 | 22 | const { category } = Astro.params; 23 | 24 | // get posts by category 25 | const BLOG_FOLDER = "blog"; 26 | const posts = await getSinglePage(BLOG_FOLDER); 27 | const filterByCategories = taxonomyFilter(posts, "categories", category!); 28 | const sortedPosts = sortByDate(filterByCategories); 29 | --- 30 | 31 | 32 | 33 |
34 |
35 |
36 | { 37 | sortedPosts.map((post) => ( 38 |
39 | 40 |
41 | )) 42 | } 43 |
44 |
45 |
46 | 47 | -------------------------------------------------------------------------------- /src/layouts/components/ImageMod.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { ImageMetadata } from "astro"; 3 | import { Image } from "astro:assets"; 4 | 5 | // Props interface for the component 6 | interface Props { 7 | src: string; 8 | alt: string; 9 | width: number; 10 | height: number; 11 | loading?: "eager" | "lazy" | null | undefined; 12 | decoding?: "async" | "auto" | "sync" | null | undefined; 13 | format?: "auto" | "avif" | "jpeg" | "png" | "svg" | "webp"; 14 | class?: string; 15 | style?: any; 16 | } 17 | 18 | // Destructuring Astro.props to get the component's props 19 | let { src, alt, width, height, loading, decoding, class: className, format, style } = Astro.props; 20 | 21 | src = `/public${src}`; 22 | 23 | // Glob pattern to load images from the /public/images folder 24 | const images = import.meta.glob("/public/images/**/*.{jpeg,jpg,png,gif,svg}"); 25 | 26 | // Check if the source path is valid 27 | const isValidPath = images[src] ? true : false; 28 | 29 | // Log a warning message in red if the image is not found 30 | !isValidPath && 31 | console.error(`\x1b[31mImage not found - ${src}.\x1b[0m Make sure the image is in the /public/images folder.`); 32 | --- 33 | 34 | { 35 | isValidPath && ( 36 | } 38 | alt={alt} 39 | width={width} 40 | height={height} 41 | loading={loading} 42 | decoding={decoding} 43 | class={className} 44 | format={format} 45 | style={style} 46 | /> 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG INSTALLER=yarn 2 | 3 | FROM node:22.20.0-alpine AS base 4 | 5 | # Install dependencies only when needed 6 | FROM base AS deps 7 | ARG INSTALLER 8 | 9 | # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. 10 | RUN apk add --no-cache libc6-compat 11 | WORKDIR /app 12 | 13 | # Install dependencies based on the preferred package manager 14 | COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./ 15 | RUN \ 16 | if [ "${INSTALLER}" == "yarn" ]; then yarn --frozen-lockfile; \ 17 | elif [ "${INSTALLER}" == "npm" ]; then npm ci; \ 18 | elif [ "${INSTALLER}" == "pnpm" ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \ 19 | else echo "Valid installer not set." && exit 1; \ 20 | fi 21 | 22 | 23 | # Rebuild the source code only when needed 24 | FROM base AS builder 25 | WORKDIR /app 26 | COPY --from=deps /app/node_modules ./node_modules 27 | COPY . . 28 | 29 | # RUN chmod u+x ./installer && ./installer 30 | ARG INSTALLER 31 | RUN \ 32 | if [ "${INSTALLER}" == "yarn" ]; then yarn build; \ 33 | elif [ "${INSTALLER}" == "npm" ]; then npm run build; \ 34 | elif [ "${INSTALLER}" == "pnpm" ]; then pnpm run build; \ 35 | else echo "Valid installer not set." && exit 1; \ 36 | fi 37 | 38 | # Production image, copy all the files and run nginx 39 | FROM nginx:alpine AS runner 40 | COPY ./config/nginx/nginx.conf /etc/nginx/nginx.conf 41 | COPY --from=builder /app/dist /usr/share/nginx/html 42 | 43 | WORKDIR /usr/share/nginx/html 44 | -------------------------------------------------------------------------------- /src/layouts/partials/CallToAction.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import ImageMod from "@/components/ImageMod.astro"; 3 | import { markdownify } from "@/lib/utils/textConverter"; 4 | const { call_to_action } = Astro.props; 5 | --- 6 | 7 | { 8 | call_to_action.data.enable && ( 9 |
10 |
11 |
12 |
13 |
14 | 22 |
23 |
24 |

28 |

32 | {call_to_action.data.button.enable && ( 33 | 37 | {call_to_action.data.button.label} 38 | 39 | )} 40 |

41 |
42 |
43 |
44 |
45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /src/content/sections/testimonial.md: -------------------------------------------------------------------------------- 1 | --- 2 | enable: true 3 | title: "What Users Are Saying About Astroplate" 4 | description: "Don't just take our word for it - hear from some of our satisfied users! Check out some of our testimonials below to see what others are saying about Astroplate." 5 | 6 | # Testimonials 7 | testimonials: 8 | - name: "Marvin McKinney" 9 | designation: "Web Designer" 10 | avatar: "/images/avatar-sm.png" 11 | content: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Qui iusto illo molestias, assumenda expedita commodi inventore non itaque molestiae voluptatum dolore, facilis sapiente, repellat veniam." 12 | 13 | - name: "Marvin McKinney" 14 | designation: "Web Designer" 15 | avatar: "/images/avatar-sm.png" 16 | content: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Qui iusto illo molestias, assumenda expedita commodi inventore non itaque molestiae voluptatum dolore, facilis sapiente, repellat veniam." 17 | 18 | - name: "Marvin McKinney" 19 | designation: "Web Designer" 20 | avatar: "/images/avatar-sm.png" 21 | content: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Qui iusto illo molestias, assumenda expedita commodi inventore non itaque molestiae voluptatum dolore, facilis sapiente, repellat veniam." 22 | 23 | - name: "Marvin McKinney" 24 | designation: "Web Designer" 25 | avatar: "/images/avatar-sm.png" 26 | content: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Qui iusto illo molestias, assumenda expedita commodi inventore non itaque molestiae voluptatum dolore, facilis sapiente, repellat veniam." 27 | --- 28 | -------------------------------------------------------------------------------- /.sitepins/schema/blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "file": "src/content/blog/post-2.md", 3 | "name": "blog", 4 | "fileType": "md", 5 | "fmType": "yaml", 6 | "template": [ 7 | { 8 | "name": "title", 9 | "label": "Title", 10 | "type": "string", 11 | "value": "", 12 | "defaultValue": "" 13 | }, 14 | { 15 | "name": "meta_title", 16 | "label": "Meta Title", 17 | "type": "string", 18 | "value": "", 19 | "defaultValue": "" 20 | }, 21 | { 22 | "name": "description", 23 | "label": "Description", 24 | "type": "string", 25 | "value": "", 26 | "defaultValue": "" 27 | }, 28 | { 29 | "name": "date", 30 | "label": "Date", 31 | "type": "Date", 32 | "value": "", 33 | "defaultValue": "", 34 | "alwaysUseCurrentDate": false 35 | }, 36 | { 37 | "name": "image", 38 | "label": "Image", 39 | "type": "media", 40 | "value": "", 41 | "defaultValue": "" 42 | }, 43 | { 44 | "name": "categories", 45 | "label": "Categories", 46 | "type": "Array", 47 | "value": [], 48 | "defaultValue": "" 49 | }, 50 | { 51 | "name": "author", 52 | "label": "Author", 53 | "type": "string", 54 | "value": "", 55 | "defaultValue": "" 56 | }, 57 | { 58 | "name": "tags", 59 | "label": "Tags", 60 | "type": "Array", 61 | "value": [], 62 | "defaultValue": "" 63 | }, 64 | { 65 | "name": "draft", 66 | "label": "Draft", 67 | "type": "boolean", 68 | "value": false, 69 | "defaultValue": "" 70 | } 71 | ] 72 | } -------------------------------------------------------------------------------- /src/config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "site": { 3 | "title": "Astroplate", 4 | "base_url": "https://astroplate.netlify.app", 5 | "base_path": "/", 6 | "trailing_slash": false, 7 | "favicon": "/images/favicon.png", 8 | "logo": "/images/logo.png", 9 | "logo_darkmode": "/images/logo-darkmode.png", 10 | "logo_width": "150", 11 | "logo_height": "30", 12 | "logo_text": "Astroplate" 13 | }, 14 | 15 | "settings": { 16 | "search": true, 17 | "sticky_header": true, 18 | "theme_switcher": true, 19 | "default_theme": "system", 20 | "pagination": 2, 21 | "summary_length": 200, 22 | "blog_folder": "blog" 23 | }, 24 | 25 | "announcement": { 26 | "enable": true, 27 | "content": "♥️ Loving Astroplate? Please ⭐️ on Github", 28 | "expire_days": 7 29 | }, 30 | 31 | "params": { 32 | "contact_form_action": "#", 33 | "copyright": "Designed And Developed by [Zeon Studio](https://zeon.studio)" 34 | }, 35 | 36 | "navigation_button": { 37 | "enable": true, 38 | "label": "Get Started", 39 | "link": "https://github.com/zeon-studio/astroplate" 40 | }, 41 | 42 | "google_tag_manager": { 43 | "enable": false, 44 | "gtm_id": "GTM-XXXXXX" 45 | }, 46 | 47 | "disqus": { 48 | "enable": true, 49 | "shortname": "themefisher-template", 50 | "settings": {} 51 | }, 52 | 53 | "metadata": { 54 | "meta_author": "zeon.studio", 55 | "meta_image": "/images/og-image.png", 56 | "meta_description": "astro and tailwind boilerplate" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/styles/navigation.css: -------------------------------------------------------------------------------- 1 | .header { 2 | @apply bg-body dark:bg-darkmode-body py-6; 3 | } 4 | 5 | /* navbar items */ 6 | .navbar { 7 | @apply relative flex flex-wrap items-center justify-between; 8 | } 9 | 10 | .navbar-brand { 11 | @apply text-text-dark dark:text-darkmode-text-dark text-xl font-semibold; 12 | image { 13 | @apply max-h-full max-w-full; 14 | } 15 | } 16 | 17 | .navbar-nav { 18 | @apply text-center lg:text-left; 19 | } 20 | 21 | .nav-link { 22 | @apply text-text-dark hover:text-primary dark:text-darkmode-text-dark dark:hover:text-darkmode-primary block p-3 cursor-pointer font-semibold transition lg:px-2 lg:py-3; 23 | } 24 | 25 | .nav-dropdown { 26 | @apply mr-0; 27 | } 28 | 29 | .nav-dropdown > svg { 30 | @apply pointer-events-none; 31 | } 32 | 33 | .nav-dropdown-list { 34 | @apply bg-body dark:bg-darkmode-body z-10 min-w-[180px] rounded p-4 shadow-sm; 35 | } 36 | 37 | .nav-dropdown-item { 38 | @apply [&:not(:last-child)]:mb-2; 39 | } 40 | 41 | .nav-dropdown-link { 42 | @apply text-text-dark hover:text-primary dark:text-darkmode-text dark:hover:text-darkmode-primary block py-1 font-semibold transition; 43 | } 44 | 45 | /* theme-switcher */ 46 | .theme-switcher { 47 | @apply inline-flex; 48 | 49 | label { 50 | @apply bg-border relative inline-block h-4 w-6 cursor-pointer rounded-2xl lg:w-10; 51 | } 52 | 53 | input { 54 | @apply absolute opacity-0; 55 | } 56 | 57 | span { 58 | @apply bg-dark absolute -top-1 left-0 flex h-6 w-6 items-center justify-center rounded-full transition-all duration-300 dark:bg-white; 59 | } 60 | 61 | input:checked + label { 62 | span { 63 | @apply lg:left-4; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/layouts/partials/PostSidebar.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { humanize } from "@/lib/utils/textConverter"; 3 | 4 | const { tags, categories, allCategories } = Astro.props; 5 | --- 6 | 7 |
8 | 9 |
10 |
Categories
11 |
12 |
    13 | { 14 | categories.map((category: any) => { 15 | const count = allCategories.filter( 16 | (c: any) => c === category 17 | ).length; 18 | return ( 19 |
  • 20 | 24 | {humanize(category)} ({count}) 25 | 26 |
  • 27 | ); 28 | }) 29 | } 30 |
31 |
32 |
33 | 34 |
35 |
Tags
36 |
37 | 53 |
54 |
55 |
56 | -------------------------------------------------------------------------------- /src/layouts/components/Share.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import config from "@/config/config.json"; 3 | import { 4 | IoLogoFacebook, 5 | IoLogoLinkedin, 6 | IoLogoPinterest, 7 | IoLogoTwitter, 8 | } from "react-icons/io5"; 9 | 10 | const { base_url }: { base_url: string } = config.site; 11 | const { 12 | title, 13 | description, 14 | slug, 15 | className, 16 | }: { title?: string; description?: string; slug?: string; className?: string } = 17 | Astro.props; 18 | --- 19 | 20 | 62 | -------------------------------------------------------------------------------- /src/lib/utils/textConverter.ts: -------------------------------------------------------------------------------- 1 | import { slug } from "github-slugger"; 2 | import { marked } from "marked"; 3 | 4 | // slugify 5 | export const slugify = (content: string) => { 6 | return slug(content); 7 | }; 8 | 9 | // markdownify 10 | export const markdownify = (content: string, div?: boolean) => { 11 | return div ? marked.parse(content) : marked.parseInline(content); 12 | }; 13 | 14 | // humanize 15 | export const humanize = (content: string) => { 16 | return content 17 | .replace(/^[\s_]+|[\s_]+$/g, "") 18 | .replace(/[_\s]+/g, " ") 19 | .replace(/[-\s]+/g, " ") 20 | .replace(/^[a-z]/, function (m) { 21 | return m.toUpperCase(); 22 | }); 23 | }; 24 | 25 | // titleify 26 | export const titleify = (content: string) => { 27 | const humanized = humanize(content); 28 | return humanized 29 | .split(" ") 30 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) 31 | .join(" "); 32 | }; 33 | 34 | // plainify 35 | export const plainify = (content: string) => { 36 | const parseMarkdown: any = marked.parse(content); 37 | const filterBrackets = parseMarkdown.replace(/<\/?[^>]+(>|$)/gm, ""); 38 | const filterSpaces = filterBrackets.replace(/[\r\n]\s*[\r\n]/gm, ""); 39 | const stripHTML = htmlEntityDecoder(filterSpaces); 40 | return stripHTML; 41 | }; 42 | 43 | // strip entities for plainify 44 | const htmlEntityDecoder = (htmlWithEntities: string) => { 45 | let entityList: { [key: string]: string } = { 46 | " ": " ", 47 | "<": "<", 48 | ">": ">", 49 | "&": "&", 50 | """: '"', 51 | "'": "'", 52 | }; 53 | let htmlWithoutEntities: string = htmlWithEntities.replace( 54 | /(&|<|>|"|')/g, 55 | (entity: string): string => { 56 | return entityList[entity]; 57 | }, 58 | ); 59 | return htmlWithoutEntities; 60 | }; 61 | -------------------------------------------------------------------------------- /src/layouts/components/Logo.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import config from "@/config/config.json"; 3 | import ImageMod from "./ImageMod.astro"; 4 | 5 | const { src, srcDarkmode }: { src?: string; srcDarkmode?: string } = 6 | Astro.props; 7 | const { 8 | logo, 9 | logo_darkmode, 10 | logo_width, 11 | logo_height, 12 | logo_text, 13 | title, 14 | }: { 15 | logo: string; 16 | logo_darkmode: string; 17 | logo_width: any; 18 | logo_height: any; 19 | logo_text: string; 20 | title: string; 21 | } = config.site; 22 | 23 | const { theme_switcher }: { theme_switcher: boolean } = config.settings; 24 | --- 25 | 26 | 27 | { 28 | src || srcDarkmode || logo || logo_darkmode ? ( 29 | <> 30 | 42 | {theme_switcher && ( 43 | 55 | )} 56 | 57 | ) : logo_text ? ( 58 | logo_text 59 | ) : ( 60 | title 61 | ) 62 | } 63 | 64 | -------------------------------------------------------------------------------- /src/layouts/components/BlogCard.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import config from "@/config/config.json"; 3 | import dateFormat from "@/lib/utils/dateFormat"; 4 | import { humanize, plainify, slugify } from "@/lib/utils/textConverter"; 5 | import { FaRegFolder, FaRegUserCircle } from "react-icons/fa"; 6 | import ImageMod from "./ImageMod.astro"; 7 | 8 | const { 9 | summary_length, 10 | blog_folder, 11 | }: { summary_length: number; blog_folder: string } = config.settings; 12 | const { data } = Astro.props; 13 | const { title, image, date, author, categories } = data.data; 14 | --- 15 | 16 |
17 | { 18 | image && ( 19 | 27 | ) 28 | } 29 |

30 | 31 | {title} 32 | 33 |

34 | 54 |

{plainify(data.body?.slice(0, Number(summary_length)))}

55 | 56 | read more 57 | 58 |
59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "astroplate", 3 | "version": "5.11.0", 4 | "description": "Astro and Tailwindcss boilerplate", 5 | "author": "zeon.studio", 6 | "license": "MIT", 7 | "packageManager": "yarn@1.22.22", 8 | "type": "module", 9 | "scripts": { 10 | "dev": "concurrently \"node scripts/themeGenerator.js --watch\" \"yarn generate-json && astro dev\"", 11 | "build": "node scripts/themeGenerator.js && yarn generate-json && astro build", 12 | "preview": "astro preview", 13 | "check": "astro check", 14 | "format": "prettier -w ./src", 15 | "generate-json": "node scripts/jsonGenerator.js", 16 | "remove-darkmode": "node scripts/removeDarkmode.js && yarn format" 17 | }, 18 | "dependencies": { 19 | "@astrojs/check": "0.9.6", 20 | "@astrojs/mdx": "4.3.13", 21 | "@astrojs/react": "4.4.2", 22 | "@astrojs/sitemap": "3.6.0", 23 | "@digi4care/astro-google-tagmanager": "^1.6.0", 24 | "@justinribeiro/lite-youtube": "^1.9.0", 25 | "astro": "5.16.5", 26 | "astro-auto-import": "^0.4.5", 27 | "astro-font": "^1.1.0", 28 | "astro-swiper": "^1.3.0", 29 | "date-fns": "^4.1.0", 30 | "disqus-react": "^1.1.7", 31 | "github-slugger": "^2.0.0", 32 | "gray-matter": "^4.0.3", 33 | "marked": "^17.0.1", 34 | "prop-types": "^15.8.1", 35 | "react": "^19.2.3", 36 | "react-dom": "^19.2.3", 37 | "react-icons": "^5.5.0", 38 | "remark-collapse": "^0.1.2", 39 | "remark-toc": "^9.0.0", 40 | "sharp": "^0.34.5", 41 | "vite": "^7.2.7" 42 | }, 43 | "devDependencies": { 44 | "@tailwindcss/forms": "^0.5.10", 45 | "@tailwindcss/typography": "^0.5.19", 46 | "@tailwindcss/vite": "^4.1.18", 47 | "@types/node": "24.10.3", 48 | "@types/react": "19.2.7", 49 | "@types/react-dom": "19.2.3", 50 | "concurrently": "^9.2.1", 51 | "eslint": "^9.39.2", 52 | "prettier": "^3.7.4", 53 | "prettier-plugin-astro": "^0.14.1", 54 | "prettier-plugin-tailwindcss": "^0.7.2", 55 | "tailwind-bootstrap-grid": "^6.0.0", 56 | "tailwindcss": "^4.1.18", 57 | "typescript": "^5.9.3" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /scripts/jsonGenerator.js: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | import matter from "gray-matter"; 4 | 5 | const CONTENT_DEPTH = 2; 6 | const JSON_FOLDER = "./.json"; 7 | const BLOG_FOLDER = "src/content/blog"; 8 | 9 | // get data from markdown 10 | const getData = (folder, groupDepth) => { 11 | const getPath = fs.readdirSync(folder); 12 | const removeIndex = getPath.filter((item) => !item.startsWith("-")); 13 | 14 | const getPaths = removeIndex.flatMap((filename) => { 15 | const filepath = path.join(folder, filename); 16 | const stats = fs.statSync(filepath); 17 | const isFolder = stats.isDirectory(); 18 | 19 | if (isFolder) { 20 | return getData(filepath, groupDepth); 21 | } else if (filename.endsWith(".md") || filename.endsWith(".mdx")) { 22 | const file = fs.readFileSync(filepath, "utf-8"); 23 | const { data, content } = matter(file); 24 | const pathParts = filepath.split(path.sep); 25 | const slug = 26 | data.slug || 27 | pathParts 28 | .slice(CONTENT_DEPTH) 29 | .join("/") 30 | .replace(/\.[^/.]+$/, ""); 31 | const group = pathParts[groupDepth]; 32 | 33 | return { 34 | group: group, 35 | slug: slug, 36 | frontmatter: data, 37 | content: content, 38 | }; 39 | } else { 40 | return []; 41 | } 42 | }); 43 | 44 | return getPaths.filter((page) => !page.frontmatter?.draft && page); 45 | }; 46 | 47 | try { 48 | // create folder if it doesn't exist 49 | if (!fs.existsSync(JSON_FOLDER)) { 50 | fs.mkdirSync(JSON_FOLDER); 51 | } 52 | 53 | // create json files 54 | fs.writeFileSync( 55 | `${JSON_FOLDER}/posts.json`, 56 | JSON.stringify(getData(BLOG_FOLDER, 2)), 57 | ); 58 | 59 | // merger json files for search 60 | const postsPath = new URL(`../${JSON_FOLDER}/posts.json`, import.meta.url); 61 | const posts = JSON.parse(fs.readFileSync(postsPath, "utf8")); 62 | const search = [...posts]; 63 | fs.writeFileSync(`${JSON_FOLDER}/search.json`, JSON.stringify(search)); 64 | } catch (err) { 65 | console.error(err); 66 | } 67 | -------------------------------------------------------------------------------- /src/pages/blog/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import BlogCard from "@/components/BlogCard.astro"; 3 | import Pagination from "@/components/Pagination.astro"; 4 | import config from "@/config/config.json"; 5 | import Base from "@/layouts/Base.astro"; 6 | import { getListPage, getSinglePage } from "@/lib/contentParser.astro"; 7 | import { getAllTaxonomy, getTaxonomy } from "@/lib/taxonomyParser.astro"; 8 | import { sortByDate } from "@/lib/utils/sortFunctions"; 9 | import PageHeader from "@/partials/PageHeader.astro"; 10 | import PostSidebar from "@/partials/PostSidebar.astro"; 11 | 12 | const BLOG_FOLDER = "blog"; 13 | 14 | const postIndex = await getListPage(BLOG_FOLDER, "-index"); 15 | if (postIndex.data.draft) { 16 | return Astro.redirect("/404"); 17 | } 18 | const posts = await getSinglePage(BLOG_FOLDER); 19 | const allCategories = await getAllTaxonomy(BLOG_FOLDER, "categories"); 20 | const categories = await getTaxonomy(BLOG_FOLDER, "categories"); 21 | const tags = await getTaxonomy(BLOG_FOLDER, "tags"); 22 | const sortedPosts = sortByDate(posts); 23 | const totalPages: number = Math.ceil(posts.length / config.settings.pagination); 24 | const currentPosts = sortedPosts.slice(0, config.settings.pagination); 25 | --- 26 | 27 | 33 | 34 |
35 |
36 |
37 | 38 |
39 |
40 | { 41 | currentPosts.map((post) => ( 42 |
43 | 44 |
45 | )) 46 | } 47 |
48 | 53 |
54 | 55 | 56 | 61 |
62 |
63 |
64 | 65 | -------------------------------------------------------------------------------- /src/content/pages/privacy-policy.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Privacy" 3 | meta_title: "" 4 | description: "this is meta description" 5 | draft: false 6 | --- 7 | 8 | #### Responsibility of Contributors 9 | 10 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Purus, donec nunc eros, ullamcorper id feugiat quisque aliquam sagittis. Sem turpis sed viverra massa gravida pharetra. Non dui dolor potenti eu dignissim fusce. Ultrices amet, in curabitur a arcu a lectus morbi id. Iaculis erat sagittis in tortor cursus. Molestie urna eu tortor, erat scelerisque eget. Nunc hendrerit sed interdum lacus. Lorem quis viverra sed 11 | 12 | pretium, aliquam sit. Praesent elementum magna amet, tincidunt eros, nibh in leo. Malesuada purus, lacus, at aliquam suspendisse tempus. Quis tempus amet, velit nascetur sollicitudin. At sollicitudin eget amet in. Eu velit nascetur sollicitudin erhdfvssfvrgss eget viverra nec elementum. Lacus, facilisis tristique lectus in. 13 | 14 | #### Gathering of Personal Information 15 | 16 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Purus, donec nunc eros, ullamcorper id feugiat quisque aliquam sagittis. Sem turpis sed viverra massa gravida pharetra. Non dui dolor potenti eu dignissim fusce. Ultrices amet, in curabitur a arcu a lectus morbi id. Iaculis erat sagittis in tortor cursus. Molestie urna eu tortor, erat scelerisque eget. Nunc hendrerit sed interdum lacus. Lorem quis viverra sed 17 | 18 | #### Protection of Personal- Information 19 | 20 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Purus, donec nunc eros, ullamcorper id feugiat quisque aliquam sagittis. Sem turpis sed viverra massa gravida pharetra. Non dui dolor potenti eu dignissim fusce. Ultrices amet, in curabitur a arcu a lectus morbi id. Iaculis erat sagittis in tortor cursus. 21 | 22 | Molestie urna eu tortor, erat scelerisque eget. Nunc hendrerit sed interdum lacus. Lorem quis viverra sed 23 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Purus, donec nunc eros, ullamcorper id feugiat 24 | 25 | #### Privacy Policy Changes 26 | 27 | 1. Sll the Themefisher items are designed to be with the latest , We check all 28 | 2. comments that threaten or harm the reputation of any person or organization 29 | 3. personal information including, but limited to, email addresses, telephone numbers 30 | 4. Any Update come in The technology Customer will get automatic Notification. 31 | -------------------------------------------------------------------------------- /src/layouts/shortcodes/Tabs.tsx: -------------------------------------------------------------------------------- 1 | import { marked } from "marked"; 2 | import React, { useEffect, useRef, useState } from "react"; 3 | 4 | const Tabs = ({ children }: { children: React.ReactElement }) => { 5 | const [active, setActive] = useState(0); 6 | const [defaultFocus, setDefaultFocus] = useState(false); 7 | 8 | const tabRefs: React.RefObject = useRef([]); 9 | useEffect(() => { 10 | if (defaultFocus) { 11 | //@ts-ignore 12 | tabRefs.current[active]?.focus(); 13 | } else { 14 | setDefaultFocus(true); 15 | } 16 | }, [active]); 17 | 18 | const tabLinks = Array.from( 19 | (children.props as any).value.matchAll( 20 | /]*>((?:.|\n)*?)<\/div>/g, 21 | ), 22 | (match: RegExpMatchArray) => ({ name: match[1], children: match[0] }), 23 | ); 24 | 25 | const handleKeyDown = ( 26 | event: React.KeyboardEvent, 27 | index: number, 28 | ) => { 29 | if (event.key === "Enter" || event.key === " ") { 30 | setActive(index); 31 | } else if (event.key === "ArrowRight") { 32 | setActive((active + 1) % tabLinks.length); 33 | } else if (event.key === "ArrowLeft") { 34 | setActive((active - 1 + tabLinks.length) % tabLinks.length); 35 | } 36 | }; 37 | 38 | return ( 39 |
40 |
    41 | {tabLinks.map( 42 | (item: { name: string; children: string }, index: number) => ( 43 |
  • handleKeyDown(event, index)} 49 | onClick={() => setActive(index)} 50 | //@ts-ignore 51 | ref={(ref) => (tabRefs.current[index] = ref)} 52 | > 53 | {item.name} 54 |
  • 55 | ), 56 | )} 57 |
58 | {tabLinks.map((item: { name: string; children: string }, i: number) => ( 59 |
66 | ))} 67 |
68 | ); 69 | }; 70 | 71 | export default Tabs; 72 | -------------------------------------------------------------------------------- /src/pages/authors/[single].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import BlogCard from "@/components/BlogCard.astro"; 3 | import ImageMod from "@/components/ImageMod.astro"; 4 | import Social from "@/components/Social.astro"; 5 | import Base from "@/layouts/Base.astro"; 6 | import { getSinglePage } from "@/lib/contentParser.astro"; 7 | import { slugify } from "@/lib/utils/textConverter"; 8 | import { render } from "astro:content"; 9 | 10 | // get all static paths for authors 11 | export async function getStaticPaths() { 12 | const COLLECTION_FOLDER = "authors"; 13 | const authors = await getSinglePage(COLLECTION_FOLDER); 14 | 15 | const paths = authors.map((author) => ({ 16 | params: { 17 | single: author.id, 18 | }, 19 | props: { author }, 20 | })); 21 | return paths; 22 | } 23 | 24 | const { author } = Astro.props; 25 | const { title, social, meta_title, description, image } = author.data; 26 | const { Content } = await render(author); 27 | 28 | // get all posts by author 29 | const BLOG_FOLDER = "blog"; 30 | const posts = await getSinglePage(BLOG_FOLDER); 31 | const postFilterByAuthor = posts.filter( 32 | (post) => slugify(post.data.author) === slugify(title) 33 | ); 34 | --- 35 | 36 | 42 |
43 |
44 |
47 |
48 | { 49 | image && ( 50 | 58 | ) 59 | } 60 |

{title}

61 |
62 | 63 |
64 | 65 |
66 |
67 | 68 |
69 | { 70 | postFilterByAuthor.map((post) => ( 71 |
72 | 73 |
74 | )) 75 | } 76 |
77 |
78 |
79 | 80 | -------------------------------------------------------------------------------- /src/pages/contact.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import config from "@/config/config.json"; 3 | import Base from "@/layouts/Base.astro"; 4 | import { getListPage } from "@/lib/contentParser.astro"; 5 | import PageHeader from "@/partials/PageHeader.astro"; 6 | 7 | const contactIndex = await getListPage("contact", "-index"); 8 | const { contact_form_action }: { contact_form_action: string } = config.params; 9 | const { title, description, meta_title, image } = contactIndex.data; 10 | 11 | if (contactIndex.data.draft) { 12 | return Astro.redirect("/404"); 13 | } 14 | --- 15 | 16 | 22 | 23 |
24 |
25 |
26 |
27 |
28 |
29 | 32 | 40 |
41 |
42 | 45 | 53 |
54 |
55 | 58 | 65 |
66 | 67 |
68 |
69 |
70 |
71 |
72 | 73 | -------------------------------------------------------------------------------- /src/content/homepage/-index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # Banner 3 | banner: 4 | title: "The Ultimate Starter Template You Need To Start Your Astro Project" 5 | content: "Astroplate is a free starter template built with Astro and TailwindCSS, providing everything you need to jumpstart your Astro project and save valuable time." 6 | image: "/images/banner.png" 7 | button: 8 | enable: true 9 | label: "Get Started For Free" 10 | link: "https://github.com/zeon-studio/astroplate" 11 | 12 | # Features 13 | features: 14 | - title: "What's Included in Astroplate" 15 | image: "/images/service-1.png" 16 | content: "Astroplate is a comprehensive starter template that includes everything you need to get started with your Astro project. What's Included in Astroplate" 17 | bulletpoints: 18 | - "10+ Pre-build pages" 19 | - "95+ Google Pagespeed Score" 20 | - "Build with Astro and TailwindCSS for easy and customizable styling" 21 | - "Fully responsive on all devices" 22 | - "SEO-optimized for better search engine rankings" 23 | - "**Open-source and free** for personal and commercial use" 24 | button: 25 | enable: false 26 | label: "Get Started Now" 27 | link: "#" 28 | 29 | - title: "Discover the Key Features Of Astro" 30 | image: "/images/service-2.png" 31 | content: "Astro is an all-in-one web framework for building fast, content-focused websites. It offers a range of exciting features for developers and website creators. Some of the key features are:" 32 | bulletpoints: 33 | - "Zero JS, by default: No JavaScript runtime overhead to slow you down." 34 | - "Customizable: Tailwind, MDX, and 100+ other integrations to choose from." 35 | - "UI-agnostic: Supports React, Preact, Svelte, Vue, Solid, Lit and more." 36 | button: 37 | enable: true 38 | label: "Get Started Now" 39 | link: "https://github.com/zeon-studio/astroplate" 40 | 41 | - title: "The Top Reasons to Choose Astro for Your Next Project" 42 | image: "/images/service-3.png" 43 | content: "With Astro, you can build modern and content-focused websites without sacrificing performance or ease of use." 44 | bulletpoints: 45 | - "Instantly load static sites for better user experience and SEO." 46 | - "Intuitive syntax and support for popular frameworks make learning and using Astro a breeze." 47 | - "Use any front-end library or framework, or build custom components, for any project size." 48 | - "Built on cutting-edge technology to keep your projects up-to-date with the latest web standards." 49 | button: 50 | enable: false 51 | label: "" 52 | link: "" 53 | --- 54 | -------------------------------------------------------------------------------- /src/layouts/helpers/Announcement.tsx: -------------------------------------------------------------------------------- 1 | import config from "@/config/config.json"; 2 | import { markdownify } from "@/lib/utils/textConverter"; 3 | import React, { useEffect, useState } from "react"; 4 | 5 | const { enable, content, expire_days } = config.announcement; 6 | 7 | const Cookies = { 8 | set: (name: string, value: string, options: any = {}) => { 9 | if (typeof document === "undefined") return; 10 | 11 | const defaults = { path: "/" }; 12 | const opts = { ...defaults, ...options }; 13 | 14 | if (typeof opts.expires === "number") { 15 | opts.expires = new Date(Date.now() + opts.expires * 864e5); 16 | } 17 | if (opts.expires instanceof Date) { 18 | opts.expires = opts.expires.toUTCString(); 19 | } 20 | 21 | let cookieString = `${encodeURIComponent(name)}=${encodeURIComponent(value)}`; 22 | 23 | for (let key in opts) { 24 | if (!opts[key]) continue; 25 | cookieString += `; ${key}`; 26 | if (opts[key] !== true) { 27 | cookieString += `=${opts[key]}`; 28 | } 29 | } 30 | 31 | document.cookie = cookieString; 32 | }, 33 | 34 | get: (name: string): string | null => { 35 | if (typeof document === "undefined") return null; 36 | 37 | const cookies = document.cookie.split("; "); 38 | for (let cookie of cookies) { 39 | const [key, value] = cookie.split("="); 40 | if (decodeURIComponent(key) === name) { 41 | return decodeURIComponent(value); 42 | } 43 | } 44 | return null; 45 | }, 46 | 47 | remove: (name: string, options: any = {}) => { 48 | Cookies.set(name, "", { ...options, expires: -1 }); 49 | }, 50 | }; 51 | 52 | const Announcement: React.FC = () => { 53 | const [isVisible, setIsVisible] = useState(false); 54 | 55 | useEffect(() => { 56 | if (enable && content && !Cookies.get("announcement-close")) { 57 | setIsVisible(true); 58 | } 59 | }, []); 60 | 61 | const handleClose = () => { 62 | Cookies.set("announcement-close", "true", { 63 | expires: expire_days, 64 | }); 65 | setIsVisible(false); 66 | }; 67 | 68 | if (!enable || !content || !isVisible) { 69 | return null; 70 | } 71 | 72 | return ( 73 |
74 |

77 | 84 |

85 | ); 86 | }; 87 | 88 | export default Announcement; 89 | -------------------------------------------------------------------------------- /src/pages/blog/page/[slug].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import BlogCard from "@/components/BlogCard.astro"; 3 | import Pagination from "@/components/Pagination.astro"; 4 | import config from "@/config/config.json"; 5 | import Base from "@/layouts/Base.astro"; 6 | import { getListPage, getSinglePage } from "@/lib/contentParser.astro"; 7 | import { getAllTaxonomy, getTaxonomy } from "@/lib/taxonomyParser.astro"; 8 | import { sortByDate } from "@/lib/utils/sortFunctions"; 9 | import PageHeader from "@/partials/PageHeader.astro"; 10 | import PostSidebar from "@/partials/PostSidebar.astro"; 11 | 12 | const BLOG_FOLDER = "blog"; 13 | 14 | const { slug } = Astro.params; 15 | const postIndex = await getListPage(BLOG_FOLDER, "-index"); 16 | 17 | if (postIndex.data.draft) { 18 | return Astro.redirect("/404"); 19 | } 20 | 21 | const posts = await getSinglePage(BLOG_FOLDER); 22 | const allCategories = await getAllTaxonomy(BLOG_FOLDER, "categories"); 23 | const categories = await getTaxonomy(BLOG_FOLDER, "categories"); 24 | const tags = await getTaxonomy(BLOG_FOLDER, "tags"); 25 | const sortedPosts = sortByDate(posts); 26 | const totalPages = Math.ceil(posts.length / config.settings.pagination); 27 | const currentPage = slug && !isNaN(Number(slug)) ? Number(slug) : 1; 28 | const indexOfLastPost = currentPage * config.settings.pagination; 29 | const indexOfFirstPost = indexOfLastPost - config.settings.pagination; 30 | const currentPosts = sortedPosts.slice(indexOfFirstPost, indexOfLastPost); 31 | 32 | export async function getStaticPaths() { 33 | const BLOG_FOLDER = "blog"; 34 | const posts = await getSinglePage(BLOG_FOLDER); 35 | const totalPages = Math.ceil(posts.length / config.settings.pagination); 36 | const paths = []; 37 | 38 | for (let i = 1; i < totalPages; i++) { 39 | paths.push({ 40 | params: { 41 | slug: (i + 1).toString(), 42 | }, 43 | }); 44 | } 45 | return paths; 46 | } 47 | --- 48 | 49 | 55 | 56 |
57 |
58 |
59 | 60 |
61 |
62 | { 63 | currentPosts.map((post) => ( 64 |
65 | 66 |
67 | )) 68 | } 69 |
70 | 75 |
76 | 77 | 78 | 83 |
84 |
85 |
86 | 87 | -------------------------------------------------------------------------------- /scripts/removeDarkmode.js: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | 4 | (function () { 5 | const rootDirs = ["src/pages", "src/hooks", "src/layouts", "src/styles"]; 6 | 7 | const deleteAssetList = [ 8 | "public/images/logo-darkmode.png", 9 | "src/layouts/components/ThemeSwitcher.astro", 10 | ]; 11 | 12 | const configFiles = [ 13 | { filePath: "src/config/theme.json", patterns: ["colors.darkmode"] }, 14 | ]; 15 | 16 | const filePaths = [ 17 | { 18 | filePath: "src/layouts/partials/Header.astro", 19 | patterns: [ 20 | "]+)?\\s*(?:\\/\\>|>([\\s\\S]*?)<\\/ThemeSwitchers*>)", 21 | ], 22 | }, 23 | ]; 24 | 25 | filePaths.forEach(({ filePath, patterns }) => { 26 | removeDarkModeFromFiles(filePath, patterns); 27 | }); 28 | 29 | deleteAssetList.forEach(deleteAsset); 30 | function deleteAsset(asset) { 31 | try { 32 | fs.unlinkSync(asset); 33 | console.log(`${path.basename(asset)} deleted successfully!`); 34 | } catch (error) { 35 | console.error(`${asset} not found`); 36 | } 37 | } 38 | 39 | rootDirs.forEach(removeDarkModeFromPages); 40 | configFiles.forEach(removeDarkMode); 41 | 42 | function removeDarkModeFromFiles(filePath, regexPatterns) { 43 | const fileContent = fs.readFileSync(filePath, "utf8"); 44 | let updatedContent = fileContent; 45 | regexPatterns.forEach((pattern) => { 46 | const regex = new RegExp(pattern, "g"); 47 | updatedContent = updatedContent.replace(regex, ""); 48 | }); 49 | fs.writeFileSync(filePath, updatedContent, "utf8"); 50 | } 51 | 52 | function removeDarkModeFromPages(directoryPath) { 53 | const files = fs.readdirSync(directoryPath); 54 | 55 | files.forEach((file) => { 56 | const filePath = path.join(directoryPath, file); 57 | const stats = fs.statSync(filePath); 58 | if (stats.isDirectory()) { 59 | removeDarkModeFromPages(filePath); 60 | } else if (stats.isFile()) { 61 | removeDarkModeFromFiles(filePath, [ 62 | '(?:(?!["])\\S)*dark:(?:(?![,;"])\\S)*', 63 | ]); 64 | } 65 | }); 66 | } 67 | 68 | function removeDarkMode(configFile) { 69 | const { filePath, patterns } = configFile; 70 | const contentFile = JSON.parse(fs.readFileSync(filePath, "utf8")); 71 | patterns.forEach((pattern) => deleteNestedProperty(contentFile, pattern)); 72 | fs.writeFileSync(filePath, JSON.stringify(contentFile)); 73 | } 74 | 75 | function deleteNestedProperty(obj, propertyPath) { 76 | const properties = propertyPath.split("."); 77 | let currentObj = obj; 78 | for (let i = 0; i < properties.length - 1; i++) { 79 | const property = properties[i]; 80 | if (currentObj.hasOwnProperty(property)) { 81 | currentObj = currentObj[property]; 82 | } else { 83 | return; // Property not found, no need to continue 84 | } 85 | } 86 | delete currentObj[properties[properties.length - 1]]; 87 | } 88 | })(); 89 | -------------------------------------------------------------------------------- /src/styles/search.css: -------------------------------------------------------------------------------- 1 | .search-modal { 2 | @apply z-50 fixed top-0 left-0 w-full h-full flex items-start justify-center invisible opacity-0; 3 | } 4 | .search-modal.show { 5 | @apply visible opacity-100; 6 | } 7 | .search-modal-overlay { 8 | @apply fixed top-0 left-0 w-full h-full bg-black opacity-50; 9 | } 10 | .search-wrapper { 11 | @apply bg-white dark:bg-darkmode-body w-[660px] max-w-[96%] mt-24 rounded shadow-lg relative z-10; 12 | } 13 | .search-wrapper-header { 14 | @apply p-4 relative; 15 | } 16 | .search-wrapper-header-input { 17 | @apply border border-solid w-full focus:ring-0 focus:border-dark border-border rounded-[4px] h-12 pr-4 pl-10 transition duration-200 outline-none dark:bg-darkmode-light dark:text-darkmode-text dark:border-darkmode-border dark:focus:border-darkmode-primary; 18 | } 19 | .search-wrapper-body { 20 | @apply dark:bg-darkmode-light dark:shadow-none max-h-[calc(100vh-350px)] overflow-y-auto bg-light shadow-[inset_0_2px_18px_#ddd] p-4 rounded; 21 | } 22 | .search-wrapper-footer { 23 | @apply text-xs select-none leading-none md:flex items-center px-3.5 py-2 hidden; 24 | } 25 | .search-wrapper-footer kbd { 26 | @apply bg-light dark:bg-darkmode-light text-xs leading-none text-center mr-[3px] px-1 py-0.5 rounded-[3px]; 27 | } 28 | .search-wrapper-footer span:not(:last-child) { 29 | @apply mr-4; 30 | } 31 | .search-wrapper-footer span:last-child { 32 | @apply ml-auto; 33 | } 34 | .search-result-empty { 35 | @apply text-center cursor-text select-none px-0 py-8; 36 | } 37 | .search-result-group { 38 | @apply mb-4; 39 | } 40 | .search-result-group-title { 41 | @apply text-lg text-text-dark dark:text-darkmode-text-dark mb-[5px] px-3; 42 | } 43 | .search-result-item { 44 | @apply rounded border bg-white dark:bg-darkmode-body dark:border-darkmode-border flex items-start mb-1 p-4 scroll-my-[30px] border-solid border-border relative; 45 | } 46 | .search-result-item mark { 47 | @apply bg-yellow-200 rounded-[2px]; 48 | } 49 | .search-result-item-title { 50 | @apply text-lg font-bold text-text-dark dark:text-darkmode-text-dark leading-none; 51 | } 52 | .search-result-item-link::after { 53 | @apply absolute top-0 right-0 bottom-0 left-0 z-10 content-[""]; 54 | } 55 | .search-result-item-image { 56 | @apply shrink-0 mr-3.5; 57 | } 58 | .search-result-item-image img { 59 | @apply w-[60px] h-[60px] md:w-[100px] md:h-[100px] rounded-[4px] object-cover; 60 | } 61 | .search-result-item-description { 62 | @apply text-sm line-clamp-1 mt-1; 63 | } 64 | .search-result-item-content { 65 | @apply mx-0 my-1.5 empty:hidden line-clamp-1; 66 | } 67 | .search-result-item-taxonomies { 68 | @apply text-sm flex flex-wrap items-center text-text-light dark:text-darkmode-text-light; 69 | } 70 | .search-result-item-taxonomies svg { 71 | @apply inline-block mr-1; 72 | } 73 | .search-result-item-active, 74 | .search-result-item:focus, 75 | .search-result-item:hover { 76 | @apply bg-dark dark:bg-dark; 77 | } 78 | .search-result-item-active .search-result-item-title, 79 | .search-result-item:focus .search-result-item-title, 80 | .search-result-item:hover .search-result-item-title { 81 | @apply text-white; 82 | } 83 | .search-result-item-active .search-result-item-description, 84 | .search-result-item:focus .search-result-item-description, 85 | .search-result-item:hover .search-result-item-description { 86 | @apply text-white/80; 87 | } 88 | .search-result-item-active .search-result-item-content, 89 | .search-result-item:focus .search-result-item-content, 90 | .search-result-item:hover .search-result-item-content { 91 | @apply text-white/90; 92 | } 93 | .search-result-item-active .search-result-item-taxonomies, 94 | .search-result-item:focus .search-result-item-taxonomies, 95 | .search-result-item:hover .search-result-item-taxonomies { 96 | @apply text-white/90; 97 | } 98 | -------------------------------------------------------------------------------- /src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import ImageMod from "@/components/ImageMod.astro"; 3 | import Base from "@/layouts/Base.astro"; 4 | import { getListPage } from "@/lib/contentParser.astro"; 5 | import { markdownify } from "@/lib/utils/textConverter"; 6 | import CallToAction from "@/partials/CallToAction.astro"; 7 | import Testimonial from "@/partials/Testimonial.astro"; 8 | import { FaCheck } from "react-icons/fa"; 9 | 10 | const homepage = await getListPage("homepage", "-index"); 11 | const call_to_action = await getListPage("ctaSection", "call-to-action"); 12 | const testimonial = await getListPage("testimonialSection", "testimonial"); 13 | 14 | const { banner, features } = homepage.data; 15 | --- 16 | 17 | 18 | 19 |
20 |
21 |
22 |
23 |

27 |

28 | { 29 | banner.button.enable && ( 30 | 38 | {banner.button.label} 39 | 40 | ) 41 | } 42 |

43 | { 44 | banner.image && ( 45 |
46 | 53 |
54 | ) 55 | } 56 |
57 |
58 |
59 | 60 | 61 | 62 | { 63 | features.map((feature, index: number) => ( 64 |
65 |
66 |
67 |
70 | 77 |
78 |
79 |

80 |

81 |

    82 | {feature.bulletpoints.map((bullet: string) => ( 83 |
  • 84 | 85 | 86 |
  • 87 | ))} 88 |
89 | {feature.button.enable && ( 90 | 91 | {feature.button.label} 92 | 93 | )} 94 |

95 |
96 |
97 |
98 | )) 99 | } 100 | 101 | 102 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | ##### Optimize default expiration time - BEGIN 2 | 3 | 4 | ## Enable expiration control 5 | ExpiresActive On 6 | 7 | ## CSS and JS expiration: 1 week after request 8 | ExpiresByType text/css "now plus 1 week" 9 | ExpiresByType application/javascript "now plus 1 week" 10 | ExpiresByType application/x-javascript "now plus 1 week" 11 | 12 | ## Image files expiration: 1 month after request 13 | ExpiresByType image/bmp "now plus 1 month" 14 | ExpiresByType image/gif "now plus 1 month" 15 | ExpiresByType image/jpeg "now plus 1 month" 16 | ExpiresByType image/webp "now plus 1 month" 17 | ExpiresByType image/jp2 "now plus 1 month" 18 | ExpiresByType image/pipeg "now plus 1 month" 19 | ExpiresByType image/png "now plus 1 month" 20 | ExpiresByType image/svg+xml "now plus 1 month" 21 | ExpiresByType image/tiff "now plus 1 month" 22 | ExpiresByType image/x-icon "now plus 1 month" 23 | ExpiresByType image/ico "now plus 1 month" 24 | ExpiresByType image/icon "now plus 1 month" 25 | ExpiresByType text/ico "now plus 1 month" 26 | ExpiresByType application/ico "now plus 1 month" 27 | ExpiresByType image/vnd.wap.wbmp "now plus 1 month" 28 | 29 | ## Font files expiration: 1 month after request 30 | ExpiresByType application/x-font-ttf "now plus 1 month" 31 | ExpiresByType application/x-font-opentype "now plus 1 month" 32 | ExpiresByType application/x-font-woff "now plus 1 month" 33 | ExpiresByType font/woff2 "now plus 1 month" 34 | ExpiresByType image/svg+xml "now plus 1 month" 35 | 36 | ## Audio files expiration: 1 month after request 37 | ExpiresByType audio/ogg "now plus 1 month" 38 | ExpiresByType application/ogg "now plus 1 month" 39 | ExpiresByType audio/basic "now plus 1 month" 40 | ExpiresByType audio/mid "now plus 1 month" 41 | ExpiresByType audio/midi "now plus 1 month" 42 | ExpiresByType audio/mpeg "now plus 1 month" 43 | ExpiresByType audio/mp3 "now plus 1 month" 44 | ExpiresByType audio/x-aiff "now plus 1 month" 45 | ExpiresByType audio/x-mpegurl "now plus 1 month" 46 | ExpiresByType audio/x-pn-realaudio "now plus 1 month" 47 | ExpiresByType audio/x-wav "now plus 1 month" 48 | 49 | ## Movie files expiration: 1 month after request 50 | ExpiresByType application/x-shockwave-flash "now plus 1 month" 51 | ExpiresByType x-world/x-vrml "now plus 1 month" 52 | ExpiresByType video/x-msvideo "now plus 1 month" 53 | ExpiresByType video/mpeg "now plus 1 month" 54 | ExpiresByType video/mp4 "now plus 1 month" 55 | ExpiresByType video/quicktime "now plus 1 month" 56 | ExpiresByType video/x-la-asf "now plus 1 month" 57 | ExpiresByType video/x-ms-asf "now plus 1 month" 58 | 59 | ##### Optimize default expiration time - END 60 | 61 | ##### 1 Month for most static resources 62 | 63 | Header set Cache-Control "max-age=2592000, public" 64 | 65 | 66 | ##### Enable gzip compression for resources 67 | 68 | mod_gzip_on Yes 69 | mod_gzip_dechunk Yes 70 | mod_gzip_item_include file .(html?|txt|css|js|php)$ 71 | mod_gzip_item_include handler ^cgi-script$ 72 | mod_gzip_item_include mime ^text/.* 73 | mod_gzip_item_include mime ^application/x-javascript.* 74 | mod_gzip_item_exclude mime ^image/.* 75 | mod_gzip_item_exclude rspheader ^Content-Encoding:.*gzip.* 76 | 77 | 78 | ##### Or, compress certain file types by extension: 79 | 80 | SetOutputFilter DEFLATE 81 | 82 | 83 | ##### Set Header Vary: Accept-Encoding 84 | 85 | 86 | Header append Vary: Accept-Encoding 87 | 88 | -------------------------------------------------------------------------------- /src/layouts/components/ThemeSwitcher.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import config from "@/config/config.json"; 3 | 4 | const { 5 | theme_switcher, 6 | // default_theme, 7 | }: { theme_switcher: boolean; default_theme: string } = config.settings; 8 | const { className }: { className?: string } = Astro.props; 9 | --- 10 | 11 | { 12 | theme_switcher && ( 13 |
14 | 15 | 43 |
44 | ) 45 | } 46 | 47 | 92 | -------------------------------------------------------------------------------- /src/layouts/PostSingle.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import BlogCard from "@/components/BlogCard.astro"; 3 | import Share from "@/components/Share.astro"; 4 | import Disqus from "@/helpers/Disqus"; 5 | import { getSinglePage } from "@/lib/contentParser.astro"; 6 | import dateFormat from "@/lib/utils/dateFormat"; 7 | import similarItems from "@/lib/utils/similarItems"; 8 | import { humanize, markdownify, slugify } from "@/lib/utils/textConverter"; 9 | import { render } from "astro:content"; 10 | import { FaRegClock, FaRegFolder, FaRegUserCircle } from "react-icons/fa"; 11 | import ImageMod from "./components/ImageMod.astro"; 12 | 13 | const COLLECTION_FOLDER = "blog"; 14 | const { post } = Astro.props; 15 | 16 | const posts = await getSinglePage(COLLECTION_FOLDER); 17 | const similarPosts = similarItems(post, posts); 18 | const { Content } = await render(post); 19 | const { title, description, author, categories, image, date, tags } = post.data; 20 | --- 21 | 22 |
23 |
24 |
25 |
26 | { 27 | image && ( 28 |
29 | 37 |
38 | ) 39 | } 40 |

41 | 64 |
65 | 66 |
67 |
68 |
69 |
Tags :
70 | 84 |
85 |
86 |
Share :
87 | 93 |
94 |
95 | 96 |

97 |
98 | 99 | 100 |
101 |

Related Posts

102 |
103 | { 104 | similarPosts.map((post) => ( 105 |
106 | 107 |
108 | )) 109 | } 110 |
111 |
112 |
113 |
114 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

Astro + Tailwind CSS + TypeScript Starter and Boilerplate

2 | 3 |

Astroplate is a free starter template built with Astro, TailwindCSS & TypeScript, providing everything you need to jumpstart your Astro project and save valuable time.

4 | 5 |

Made with ♥ by Zeon Studio

6 | 7 |

If you find this project useful, please give it a ⭐ to show your support.

8 | 9 |

👀 Demo | 👀 Demo Multilang | Page Speed (100%)🚀 10 |

11 | 12 |

13 | 14 | Astro Version 5.15 15 | 16 | 17 | 18 | license 19 | 20 | code size 21 | 22 | 23 | contributors 24 |

25 | 26 | ## 📌 Key Features 27 | 28 | - 👥 Multi-Authors 29 | - 🌐 Multilingual 30 | - 🎯 Similar Posts Suggestion 31 | - 🔍 Search Functionality 32 | - 🌑 Dark Mode 33 | - 🏷️ Tags & Categories 34 | - 🔗 Netlify setting pre-configured 35 | - 📞 Support contact form 36 | - 📱 Fully responsive 37 | - 📝 Write and update content in Markdown / MDX 38 | - 📎 Google Tag Manager 39 | - 💬 Disqus Comment 40 | - 🔳 Syntax Highlighting 41 | 42 | ### 📄 15+ Pre-designed Pages 43 | 44 | - 🏠 Homepage 45 | - 👤 About 46 | - 📞 Contact 47 | - 👥 Authors 48 | - 👤 Author Single 49 | - 📝 Blog 50 | - 📝 Blog Single 51 | - 🚫 Custom 404 52 | - 💡 Elements 53 | - 📄 Privacy Policy 54 | - 🏷️ Tags 55 | - 🏷️ Tag Single 56 | - 🗂️ Categories 57 | - 🗂️ Category Single 58 | - 🔍 Search 59 | 60 | ## 🔗 Integrations 61 | 62 | - astro/react 63 | - astro/sitemap 64 | - astro/tailwind 65 | 66 | ## 🚀 Getting Started 67 | 68 | ### 📦 Dependencies 69 | 70 | - astro v5.15+ 71 | - node v20.10+ 72 | - yarn v1.22+ 73 | - tailwind v4+ 74 | 75 | ### 👉 Install Dependencies 76 | 77 | ```bash 78 | yarn install 79 | ``` 80 | 81 | ### 👉 Development Command 82 | 83 | ```bash 84 | yarn run dev 85 | ``` 86 | 87 | ### 👉 Build Command 88 | 89 | ```bash 90 | yarn run build 91 | ``` 92 | 93 | ### 👉 Build and Run With Docker 94 | 95 | ```bash 96 | docker build -t astroplate . 97 | # or 98 | # docker --build-arg INSTALLER=npm build -t astroplate . 99 | # or 100 | # docker --build-arg INSTALLER=pnpm build -t astroplate . 101 | 102 | docker run -p 3000:80 astroplate 103 | # or 104 | # docker run --rm -p 3000:80 astroplate 105 | ``` 106 | 107 | To access the shell within the container: 108 | 109 | ```bash 110 | docker run -it --rm astroplate ash 111 | ``` 112 | 113 | 114 | 115 | ## 🐞 Reporting Issues 116 | 117 | We use GitHub Issues as the official bug tracker for this Template. Please Search [existing issues](https://github.com/zeon-studio/astroplate/issues). It’s possible someone has already reported the same problem. 118 | If your problem or idea has not been addressed yet, feel free to [open a new issue](https://github.com/zeon-studio/astroplate/issues). 119 | 120 | 121 | 122 | ## 📝 License 123 | 124 | Copyright (c) 2023 - Present, Designed & Developed by [Zeon Studio](https://zeon.studio/) 125 | 126 | **Code License:** Released under the [MIT](https://github.com/zeon-studio/astroplate/blob/main/LICENSE) license. 127 | 128 | **Image license:** The images are only for demonstration purposes. They have their license, we don't have permission to share those images. 129 | 130 | ## 💻 Need Custom Development Services? 131 | 132 | If you need a custom theme, theme customization, or complete website development services from scratch you can [Hire Us](https://zeon.studio/). 133 | -------------------------------------------------------------------------------- /src/layouts/shortcodes/Notice.tsx: -------------------------------------------------------------------------------- 1 | import { humanize } from "@/lib/utils/textConverter"; 2 | import React from "react"; 3 | 4 | function Notice({ 5 | type, 6 | children, 7 | }: { 8 | type: string; 9 | children: React.ReactNode; 10 | }) { 11 | return ( 12 |
13 |
14 | {type === "tip" ? ( 15 | 22 | 28 | 29 | ) : type === "info" ? ( 30 | 37 | 41 | 45 | 46 | ) : type === "warning" ? ( 47 | 54 | 60 | 61 | ) : ( 62 | 69 | 76 | 77 | )} 78 |

{humanize(type)}

79 |
80 |
{children}
81 |
82 | ); 83 | } 84 | 85 | export default Notice; 86 | -------------------------------------------------------------------------------- /src/content.config.ts: -------------------------------------------------------------------------------- 1 | import { glob } from "astro/loaders"; 2 | import { defineCollection, z } from "astro:content"; 3 | 4 | const commonFields = { 5 | title: z.string(), 6 | description: z.string(), 7 | meta_title: z.string().optional(), 8 | date: z.date().optional(), 9 | image: z.string().optional(), 10 | draft: z.boolean(), 11 | }; 12 | 13 | // Post collection schema 14 | const blogCollection = defineCollection({ 15 | loader: glob({ pattern: "**/*.{md,mdx}", base: "src/content/blog" }), 16 | schema: z.object({ 17 | title: z.string(), 18 | meta_title: z.string().optional(), 19 | description: z.string().optional(), 20 | date: z.date().optional(), 21 | image: z.string().optional(), 22 | author: z.string().default("Admin"), 23 | categories: z.array(z.string()).default(["others"]), 24 | tags: z.array(z.string()).default(["others"]), 25 | draft: z.boolean().optional(), 26 | }), 27 | }); 28 | 29 | // Author collection schema 30 | const authorsCollection = defineCollection({ 31 | loader: glob({ pattern: "**/*.{md,mdx}", base: "src/content/authors" }), 32 | schema: z.object({ 33 | ...commonFields, 34 | social: z 35 | .array( 36 | z 37 | .object({ 38 | name: z.string().optional(), 39 | icon: z.string().optional(), 40 | link: z.string().optional(), 41 | }) 42 | .optional(), 43 | ) 44 | .optional(), 45 | draft: z.boolean().optional(), 46 | }), 47 | }); 48 | 49 | // Pages collection schema 50 | const pagesCollection = defineCollection({ 51 | loader: glob({ pattern: "**/*.{md,mdx}", base: "src/content/pages" }), 52 | schema: z.object({ 53 | ...commonFields, 54 | }), 55 | }); 56 | 57 | // about collection schema 58 | const aboutCollection = defineCollection({ 59 | loader: glob({ pattern: "**/*.{md,mdx}", base: "src/content/about" }), 60 | schema: z.object({ 61 | ...commonFields, 62 | }), 63 | }); 64 | 65 | // contact collection schema 66 | const contactCollection = defineCollection({ 67 | loader: glob({ pattern: "**/*.{md,mdx}", base: "src/content/contact" }), 68 | schema: z.object({ 69 | ...commonFields, 70 | }), 71 | }); 72 | 73 | // Homepage collection schema 74 | const homepageCollection = defineCollection({ 75 | loader: glob({ pattern: "**/-*.{md,mdx}", base: "src/content/homepage" }), 76 | schema: z.object({ 77 | banner: z.object({ 78 | title: z.string(), 79 | content: z.string(), 80 | image: z.string(), 81 | button: z.object({ 82 | enable: z.boolean(), 83 | label: z.string(), 84 | link: z.string(), 85 | }), 86 | }), 87 | features: z.array( 88 | z.object({ 89 | title: z.string(), 90 | image: z.string(), 91 | content: z.string(), 92 | bulletpoints: z.array(z.string()), 93 | button: z.object({ 94 | enable: z.boolean(), 95 | label: z.string(), 96 | link: z.string(), 97 | }), 98 | }), 99 | ), 100 | }), 101 | }); 102 | 103 | // Call to Action collection schema 104 | const ctaSectionCollection = defineCollection({ 105 | loader: glob({ 106 | pattern: "call-to-action.{md,mdx}", 107 | base: "src/content/sections", 108 | }), 109 | schema: z.object({ 110 | enable: z.boolean(), 111 | title: z.string(), 112 | description: z.string(), 113 | image: z.string(), 114 | button: z.object({ 115 | enable: z.boolean(), 116 | label: z.string(), 117 | link: z.string(), 118 | }), 119 | }), 120 | }); 121 | 122 | // Testimonials Section collection schema 123 | const testimonialSectionCollection = defineCollection({ 124 | loader: glob({ 125 | pattern: "testimonial.{md,mdx}", 126 | base: "src/content/sections", 127 | }), 128 | schema: z.object({ 129 | enable: z.boolean(), 130 | title: z.string(), 131 | description: z.string(), 132 | testimonials: z.array( 133 | z.object({ 134 | name: z.string(), 135 | avatar: z.string(), 136 | designation: z.string(), 137 | content: z.string(), 138 | }), 139 | ), 140 | }), 141 | }); 142 | 143 | // Export collections 144 | export const collections = { 145 | // Pages 146 | homepage: homepageCollection, 147 | blog: blogCollection, 148 | authors: authorsCollection, 149 | pages: pagesCollection, 150 | about: aboutCollection, 151 | contact: contactCollection, 152 | 153 | // sections 154 | ctaSection: ctaSectionCollection, 155 | testimonialSection: testimonialSectionCollection, 156 | }; 157 | -------------------------------------------------------------------------------- /src/layouts/components/Pagination.astro: -------------------------------------------------------------------------------- 1 | --- 2 | type Pagination = { 3 | section?: string; 4 | currentPage?: number; 5 | totalPages?: number; 6 | }; 7 | const { section, currentPage = 1, totalPages = 1 }: Pagination = Astro.props; 8 | 9 | const indexPageLink = currentPage === 2; 10 | const hasPrevPage = currentPage > 1; 11 | const hasNextPage = totalPages > currentPage!; 12 | 13 | let pageList: number[] = []; 14 | for (let i = 1; i <= totalPages; i++) { 15 | pageList.push(i); 16 | } 17 | --- 18 | 19 | { 20 | totalPages > 1 && ( 21 | 133 | ) 134 | } 135 | -------------------------------------------------------------------------------- /src/layouts/partials/Testimonial.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import ImageMod from "@/components/ImageMod.astro"; 3 | import { markdownify } from "@/lib/utils/textConverter"; 4 | import { 5 | Swiper, 6 | SwiperPagination, 7 | SwiperSlide, 8 | SwiperWrapper, 9 | } from "astro-swiper"; 10 | import type { SwiperOptions } from "swiper/types"; 11 | 12 | const { testimonial } = Astro.props; 13 | 14 | const swiperOptions: SwiperOptions = { 15 | spaceBetween: 24, 16 | loop: true, 17 | autoplay: { 18 | delay: 2500, 19 | disableOnInteraction: false, 20 | }, 21 | pagination: { 22 | el: ".testimonial-slider-pagination", 23 | type: "bullets", 24 | clickable: true, 25 | dynamicBullets: false, 26 | }, 27 | breakpoints: { 28 | 768: { 29 | slidesPerView: 2, 30 | }, 31 | 992: { 32 | slidesPerView: 3, 33 | }, 34 | }, 35 | }; 36 | --- 37 | 38 | { 39 | testimonial.data.enable && ( 40 |
41 |
42 |
43 |
44 |

45 |

46 |

47 |
48 | 49 | 50 | {testimonial.data.testimonials.map( 51 | (item: { 52 | avatar: string; 53 | content: string; 54 | name: string; 55 | designation: string; 56 | }) => ( 57 | 58 |
59 |
60 | 67 | 71 | 72 |
73 |
77 |
78 |
79 | 87 |
88 |
89 |

93 |

97 |

98 |
99 |
100 |
101 | ) 102 | )} 103 |
104 | 105 |
106 |
107 |
108 |
109 |
110 | ) 111 | } 112 | -------------------------------------------------------------------------------- /src/layouts/partials/Header.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Logo from "@/components/Logo.astro"; 3 | import ThemeSwitcher from "@/components/ThemeSwitcher.astro"; 4 | import config from "@/config/config.json"; 5 | import menu from "@/config/menu.json"; 6 | import { IoSearch } from "react-icons/io5"; 7 | 8 | export interface ChildNavigationLink { 9 | name: string; 10 | url: string; 11 | } 12 | 13 | export interface NavigationLink { 14 | name: string; 15 | url: string; 16 | hasChildren?: boolean; 17 | children?: ChildNavigationLink[]; 18 | } 19 | 20 | const { main }: { main: NavigationLink[] } = menu; 21 | const { navigation_button, settings } = config; 22 | const { pathname } = Astro.url; 23 | --- 24 | 25 |
26 | 151 |
152 | -------------------------------------------------------------------------------- /src/styles/components.css: -------------------------------------------------------------------------------- 1 | /* section style */ 2 | .section { 3 | @apply py-24 xl:py-28; 4 | } 5 | 6 | .section-sm { 7 | @apply py-16 xl:py-20; 8 | } 9 | 10 | /* container */ 11 | .container { 12 | @apply mx-auto xl:!max-w-[1320px] px-4; 13 | } 14 | 15 | /* social icons */ 16 | .social-icons { 17 | @apply space-x-4; 18 | } 19 | .social-icons li { 20 | @apply inline-block; 21 | } 22 | .social-icons li a { 23 | @apply flex h-9 w-9 items-center justify-center rounded-sm bg-primary text-center leading-9 text-white dark:bg-darkmode-primary dark:text-text-dark; 24 | } 25 | .social-icons li a svg { 26 | @apply h-5 w-5; 27 | } 28 | 29 | /* notice */ 30 | .notice { 31 | @apply mb-6 rounded-lg border px-8 py-6; 32 | } 33 | 34 | .notice-head { 35 | @apply flex items-center; 36 | } 37 | 38 | .notice-head svg { 39 | @apply mr-3; 40 | } 41 | 42 | .notice-head p { 43 | @apply font-secondary text-xl font-semibold text-text-dark dark:text-darkmode-text-light; 44 | } 45 | 46 | .notice-body { 47 | @apply mt-3; 48 | } 49 | 50 | .notice-body p { 51 | @apply my-0; 52 | } 53 | 54 | .notice.note { 55 | @apply text-[#1B83E2]; 56 | @apply border-current; 57 | } 58 | 59 | .notice.tip { 60 | @apply text-[#40D294]; 61 | @apply border-current; 62 | } 63 | 64 | .notice.info { 65 | @apply text-[#E3A72C]; 66 | @apply border-current; 67 | } 68 | 69 | .notice.warning { 70 | @apply text-[#DB2C23]; 71 | @apply border-current; 72 | } 73 | 74 | /* tab */ 75 | .tab { 76 | @apply overflow-hidden rounded-lg border border-border dark:border-darkmode-border; 77 | } 78 | 79 | .tab-nav { 80 | @apply flex border-b border-border bg-light dark:border-darkmode-border dark:bg-darkmode-light !m-0 !list-none; 81 | } 82 | 83 | .tab-nav-item { 84 | @apply cursor-pointer border-b-[3px] border-border py-2 text-lg text-text-dark opacity-80 dark:border-darkmode-border !my-0 !px-8; 85 | } 86 | 87 | .tab-nav-item.active { 88 | @apply border-b-[3px] border-dark opacity-100 dark:border-darkmode-primary; 89 | } 90 | 91 | .tab-content { 92 | @apply px-5; 93 | } 94 | 95 | .tab-content-panel { 96 | @apply p-8; 97 | } 98 | 99 | .tab-content-panel p { 100 | @apply mb-0; 101 | } 102 | 103 | .tab-content-panel.active { 104 | @apply block; 105 | } 106 | 107 | /* accordion */ 108 | .accordion { 109 | @apply mb-6 overflow-hidden rounded-lg border border-border bg-light dark:border-darkmode-border dark:bg-darkmode-light; 110 | } 111 | 112 | .accordion-header { 113 | @apply flex w-full cursor-pointer items-center justify-between px-8 py-4 text-lg text-text-dark dark:bg-darkmode-light dark:text-darkmode-text-light; 114 | } 115 | 116 | .accordion-icon { 117 | @apply h-[.8em] w-[.8em] rotate-[-90deg] transition-transform duration-200; 118 | } 119 | 120 | .accordion-content { 121 | @apply max-h-0 overflow-hidden px-8 py-0; 122 | } 123 | 124 | .accordion.active .accordion-icon { 125 | @apply rotate-0; 126 | } 127 | 128 | .accordion.active .accordion-content { 129 | @apply max-h-screen; 130 | } 131 | 132 | /* modal */ 133 | .modal { 134 | @apply fixed inset-0 z-40 hidden h-full w-full overflow-auto; 135 | } 136 | 137 | .modal-overlay { 138 | @apply fixed inset-0 z-40 hidden h-full w-full bg-black/40; 139 | } 140 | 141 | .modal-content { 142 | @apply relative top-1/2 z-50 mx-auto max-w-[90%] -translate-y-1/2 rounded-lg bg-body p-8 dark:bg-darkmode-body; 143 | } 144 | 145 | .modal-close { 146 | @apply absolute right-3 top-3 h-8 w-8 rounded-full bg-light text-center leading-8 text-text-dark dark:bg-darkmode-light dark:text-darkmode-text-dark; 147 | } 148 | 149 | /* content style */ 150 | .content { 151 | @apply prose max-w-none; 152 | @apply prose-headings:mb-[.3em] prose-headings:mt-[.6em] prose-headings:text-text-dark dark:prose-headings:text-darkmode-text-dark; 153 | @apply prose-h1:text-h1-sm md:prose-h1:text-h1; 154 | @apply prose-h2:text-h2-sm md:prose-h2:text-h2; 155 | @apply prose-h3:text-h3-sm md:prose-h3:text-h3; 156 | @apply prose-p:text-base prose-p:text-text dark:prose-p:text-darkmode-text; 157 | @apply prose-a:text-text prose-a:[&.btn]:no-underline dark:prose-a:text-darkmode-text; 158 | @apply prose-img:max-w-full prose-img:rounded; 159 | @apply prose-strong:text-text-dark dark:prose-strong:text-darkmode-text; 160 | @apply prose-hr:border-border dark:prose-hr:border-darkmode-border; 161 | @apply prose-pre:rounded-lg prose-pre:bg-light dark:prose-pre:bg-darkmode-light; 162 | @apply prose-code:text-darkmode-text-dark; 163 | @apply prose-li:text-text dark:prose-li:text-darkmode-text; 164 | @apply prose-blockquote:rounded-lg prose-blockquote:border prose-blockquote:border-l-[10px] prose-blockquote:border-primary prose-blockquote:bg-light prose-blockquote:px-8 prose-blockquote:py-10 prose-blockquote:font-secondary prose-blockquote:text-2xl prose-blockquote:not-italic prose-blockquote:text-text-dark dark:prose-blockquote:border-darkmode-primary dark:prose-blockquote:bg-darkmode-light dark:prose-blockquote:text-darkmode-text-light; 165 | @apply prose-table:relative prose-table:overflow-hidden prose-table:rounded-lg prose-table:before:absolute prose-table:before:left-0 prose-table:before:top-0 prose-table:before:h-full prose-table:before:w-full prose-table:before:rounded-[inherit] prose-table:before:border prose-table:before:border-border prose-table:before:content-[""] dark:prose-table:before:border-darkmode-border; 166 | @apply prose-thead:border-border prose-thead:bg-light dark:prose-thead:border-darkmode-border dark:prose-thead:bg-darkmode-light; 167 | @apply prose-th:relative prose-th:z-10 prose-th:px-4 prose-th:py-[18px] prose-th:text-text-dark dark:prose-th:text-darkmode-text; 168 | @apply prose-tr:border-border dark:prose-tr:border-darkmode-border; 169 | @apply prose-td:relative prose-td:z-10 prose-td:px-3 prose-td:py-[18px] dark:prose-td:text-darkmode-text; 170 | } 171 | -------------------------------------------------------------------------------- /src/layouts/Base.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import TwSizeIndicator from "@/components/TwSizeIndicator.astro"; 3 | import config from "@/config/config.json"; 4 | import theme from "@/config/theme.json"; 5 | import { plainify } from "@/lib/utils/textConverter"; 6 | import Footer from "@/partials/Footer.astro"; 7 | import Header from "@/partials/Header.astro"; 8 | import "@/styles/main.css"; 9 | import { 10 | GoogleTagmanager, 11 | GoogleTagmanagerNoscript, 12 | } from "@digi4care/astro-google-tagmanager"; 13 | import { AstroFont } from "astro-font"; 14 | import { ClientRouter } from "astro:transitions"; 15 | import Announcement from "./helpers/Announcement"; 16 | import SearchModal from "./helpers/SearchModal"; 17 | 18 | // font families 19 | const pf = theme.fonts.font_family.primary; 20 | const sf = theme.fonts.font_family.secondary; 21 | 22 | let fontPrimary, fontSecondary; 23 | if (theme.fonts.font_family.primary) { 24 | fontPrimary = theme.fonts.font_family.primary 25 | .replace(/\+/g, " ") 26 | .replace(/:[ital,]*[ital@]*[wght@]*[0-9,;.]+/gi, ""); 27 | } 28 | if (theme.fonts.font_family.secondary) { 29 | fontSecondary = theme.fonts.font_family.secondary 30 | .replace(/\+/g, " ") 31 | .replace(/:[ital,]*[ital@]*[wght@]*[0-9,;.]+/gi, ""); 32 | } 33 | 34 | // types for frontmatters 35 | export interface Props { 36 | title?: string; 37 | meta_title?: string; 38 | description?: string; 39 | image?: string; 40 | noindex?: boolean; 41 | canonical?: string; 42 | } 43 | 44 | // destructure frontmatter 45 | const { title, meta_title, description, image, noindex, canonical } = 46 | Astro.props; 47 | --- 48 | 49 | 50 | 51 | 52 | 53 | { 54 | config.google_tag_manager.enable && ( 55 | 56 | ) 57 | } 58 | 59 | 60 | 61 | 62 | 63 | 68 | 73 | 74 | 75 | 76 | 77 | 99 | 100 | 101 | 105 | 106 | 107 | 108 | {plainify(meta_title ? meta_title : title ? title : config.site.title)} 109 | 110 | 111 | 112 | {canonical && } 113 | 114 | 115 | {noindex && } 116 | 117 | 118 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 137 | 138 | 139 | 145 | 146 | 150 | 151 | 152 | 158 | 159 | 160 | 166 | 167 | 168 | 174 | 175 | 176 | 182 | 183 | 184 | 185 | {/* google tag manager noscript */} 186 | { 187 | config.google_tag_manager.enable && ( 188 | 189 | ) 190 | } 191 | 192 | 193 | 194 |
195 | 196 |
197 | 198 |
199 |