├── .dockerignore ├── .editorconfig ├── .github └── workflows │ └── actions.yaml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc.cjs ├── .stackblitzrc ├── .vscode ├── astrowind │ └── config-schema.json ├── extensions.json ├── launch.json └── settings.json ├── Dockerfile ├── LICENSE.md ├── README.md ├── astro.config.ts ├── docker-compose.yml ├── eslint.config.js ├── netlify.toml ├── nginx └── nginx.conf ├── package-lock.json ├── package.json ├── public ├── _headers ├── decapcms │ ├── config.yml │ └── index.html └── robots.txt ├── sandbox.config.json ├── src ├── assets │ ├── favicons │ │ ├── apple-touch-icon.png │ │ ├── favicon.ico │ │ └── favicon.svg │ ├── images │ │ ├── app-store.png │ │ ├── default.png │ │ ├── google-play.png │ │ └── hero-image.png │ └── styles │ │ └── tailwind.css ├── components │ ├── CustomStyles.astro │ ├── Favicons.astro │ ├── Logo.astro │ ├── blog │ │ ├── Grid.astro │ │ ├── GridItem.astro │ │ ├── Headline.astro │ │ ├── List.astro │ │ ├── ListItem.astro │ │ ├── Pagination.astro │ │ ├── RelatedPosts.astro │ │ ├── SinglePost.astro │ │ ├── Tags.astro │ │ └── ToBlogLink.astro │ ├── common │ │ ├── Analytics.astro │ │ ├── ApplyColorMode.astro │ │ ├── BasicScripts.astro │ │ ├── CommonMeta.astro │ │ ├── Image.astro │ │ ├── Metadata.astro │ │ ├── SiteVerification.astro │ │ ├── SocialShare.astro │ │ ├── SplitbeeAnalytics.astro │ │ ├── ToggleMenu.astro │ │ └── ToggleTheme.astro │ ├── ui │ │ ├── Background.astro │ │ ├── Button.astro │ │ ├── DListItem.astro │ │ ├── Form.astro │ │ ├── Headline.astro │ │ ├── ItemGrid.astro │ │ ├── ItemGrid2.astro │ │ ├── Timeline.astro │ │ └── WidgetWrapper.astro │ └── widgets │ │ ├── Announcement.astro │ │ ├── BlogHighlightedPosts.astro │ │ ├── BlogLatestPosts.astro │ │ ├── Brands.astro │ │ ├── CallToAction.astro │ │ ├── Contact.astro │ │ ├── Content.astro │ │ ├── FAQs.astro │ │ ├── Features.astro │ │ ├── Features2.astro │ │ ├── Features3.astro │ │ ├── Footer.astro │ │ ├── Header.astro │ │ ├── Hero.astro │ │ ├── Hero2.astro │ │ ├── HeroText.astro │ │ ├── Note.astro │ │ ├── Pricing.astro │ │ ├── Stats.astro │ │ ├── Steps.astro │ │ ├── Steps2.astro │ │ └── Testimonials.astro ├── config.yaml ├── content │ └── config.ts ├── data │ └── post │ │ ├── astrowind-template-in-depth.mdx │ │ ├── get-started-website-with-astro-tailwind-css.md │ │ ├── how-to-customize-astrowind-to-your-brand.md │ │ ├── landing.md │ │ ├── markdown-elements-demo-post.mdx │ │ └── useful-resources-to-create-websites.md ├── env.d.ts ├── layouts │ ├── LandingLayout.astro │ ├── Layout.astro │ ├── MarkdownLayout.astro │ └── PageLayout.astro ├── navigation.ts ├── pages │ ├── 404.astro │ ├── [...blog] │ │ ├── [...page].astro │ │ ├── [category] │ │ │ └── [...page].astro │ │ ├── [tag] │ │ │ └── [...page].astro │ │ └── index.astro │ ├── about.astro │ ├── contact.astro │ ├── homes │ │ ├── mobile-app.astro │ │ ├── personal.astro │ │ ├── saas.astro │ │ └── startup.astro │ ├── index.astro │ ├── landing │ │ ├── click-through.astro │ │ ├── lead-generation.astro │ │ ├── pre-launch.astro │ │ ├── product.astro │ │ ├── sales.astro │ │ └── subscription.astro │ ├── pricing.astro │ ├── privacy.md │ ├── rss.xml.ts │ ├── services.astro │ └── terms.md ├── types.d.ts └── utils │ ├── blog.ts │ ├── directories.ts │ ├── frontmatter.ts │ ├── images-optimization.ts │ ├── images.ts │ ├── permalinks.ts │ └── utils.ts ├── tailwind.config.js ├── tsconfig.json ├── vendor ├── README.md └── integration │ ├── index.ts │ ├── types.d.ts │ └── utils │ ├── configBuilder.ts │ └── loadConfig.ts ├── vercel.json └── vscode.tailwind.json /.dockerignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | .vscode/ 4 | .github/ 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | indent_size = 2 10 | indent_style = space 11 | insert_final_newline = true 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.github/workflows/actions.yaml: -------------------------------------------------------------------------------- 1 | name: GitHub Actions 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | node-version: 17 | - 18 18 | - 20 19 | - 22 20 | steps: 21 | - uses: actions/checkout@v4 22 | - name: Use Node.js v${{ matrix.node-version }} 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | cache: npm 27 | - run: npm ci 28 | - run: npm run build 29 | # - run: npm test 30 | 31 | check: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v4 35 | - name: Use Node.js 22 36 | uses: actions/setup-node@v4 37 | with: 38 | node-version: 22 39 | cache: npm 40 | - run: npm ci 41 | - run: npm run check 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | .output/ 4 | 5 | # dependencies 6 | node_modules/ 7 | 8 | # logs 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | pnpm-debug.log* 13 | 14 | 15 | # environment variables 16 | .env 17 | .env.production 18 | 19 | # macOS-specific files 20 | .DS_Store 21 | 22 | pnpm-lock.yaml 23 | 24 | .astro -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # Expose Astro dependencies for `pnpm` users 2 | shamefully-hoist=true -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .github 4 | .changeset -------------------------------------------------------------------------------- /.prettierrc.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('prettier').Config} */ 2 | module.exports = { 3 | printWidth: 120, 4 | semi: true, 5 | singleQuote: true, 6 | tabWidth: 2, 7 | trailingComma: 'es5', 8 | useTabs: false, 9 | 10 | plugins: [require.resolve('prettier-plugin-astro')], 11 | 12 | overrides: [{ files: '*.astro', options: { parser: 'astro' } }], 13 | }; 14 | -------------------------------------------------------------------------------- /.stackblitzrc: -------------------------------------------------------------------------------- 1 | { 2 | "startCommand": "npm start", 3 | "env": { 4 | "ENABLE_CJS_IMPORTS": true 5 | } 6 | } -------------------------------------------------------------------------------- /.vscode/astrowind/config-schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "type": "object", 4 | "properties": { 5 | "site": { 6 | "type": "object", 7 | "properties": { 8 | "name": { 9 | "type": "string" 10 | }, 11 | "site": { 12 | "type": "string" 13 | }, 14 | "base": { 15 | "type": "string" 16 | }, 17 | "trailingSlash": { 18 | "type": "boolean" 19 | }, 20 | "googleSiteVerificationId": { 21 | "type": "string" 22 | } 23 | }, 24 | "required": ["name", "site", "base", "trailingSlash"], 25 | "additionalProperties": false 26 | }, 27 | "metadata": { 28 | "type": "object", 29 | "properties": { 30 | "title": { 31 | "type": "object", 32 | "properties": { 33 | "default": { 34 | "type": "string" 35 | }, 36 | "template": { 37 | "type": "string" 38 | } 39 | }, 40 | "required": ["default", "template"] 41 | }, 42 | "description": { 43 | "type": "string" 44 | }, 45 | "robots": { 46 | "type": "object", 47 | "properties": { 48 | "index": { 49 | "type": "boolean" 50 | }, 51 | "follow": { 52 | "type": "boolean" 53 | } 54 | }, 55 | "required": ["index", "follow"] 56 | }, 57 | "openGraph": { 58 | "type": "object", 59 | "properties": { 60 | "site_name": { 61 | "type": "string" 62 | }, 63 | "images": { 64 | "type": "array", 65 | "items": [ 66 | { 67 | "type": "object", 68 | "properties": { 69 | "url": { 70 | "type": "string" 71 | }, 72 | "width": { 73 | "type": "integer" 74 | }, 75 | "height": { 76 | "type": "integer" 77 | } 78 | }, 79 | "required": ["url", "width", "height"] 80 | } 81 | ] 82 | }, 83 | "type": { 84 | "type": "string" 85 | } 86 | }, 87 | "required": ["site_name", "images", "type"] 88 | }, 89 | "twitter": { 90 | "type": "object", 91 | "properties": { 92 | "handle": { 93 | "type": "string" 94 | }, 95 | "site": { 96 | "type": "string" 97 | }, 98 | "cardType": { 99 | "type": "string" 100 | } 101 | }, 102 | "required": ["handle", "site", "cardType"] 103 | } 104 | }, 105 | "required": ["title", "description", "robots", "openGraph", "twitter"] 106 | }, 107 | "i18n": { 108 | "type": "object", 109 | "properties": { 110 | "language": { 111 | "type": "string" 112 | }, 113 | "textDirection": { 114 | "type": "string" 115 | } 116 | }, 117 | "required": ["language", "textDirection"] 118 | }, 119 | "apps": { 120 | "type": "object", 121 | "properties": { 122 | "blog": { 123 | "type": "object", 124 | "properties": { 125 | "isEnabled": { 126 | "type": "boolean" 127 | }, 128 | "postsPerPage": { 129 | "type": "integer" 130 | }, 131 | "isRelatedPostsEnabled": { 132 | "type": "boolean" 133 | }, 134 | "relatedPostsCount": { 135 | "type": "integer" 136 | }, 137 | "post": { 138 | "type": "object", 139 | "properties": { 140 | "isEnabled": { 141 | "type": "boolean" 142 | }, 143 | "permalink": { 144 | "type": "string" 145 | }, 146 | "robots": { 147 | "type": "object", 148 | "properties": { 149 | "index": { 150 | "type": "boolean" 151 | }, 152 | "follow": { 153 | "type": "boolean" 154 | } 155 | }, 156 | "required": ["index"] 157 | } 158 | }, 159 | "required": ["isEnabled", "permalink", "robots"] 160 | }, 161 | "list": { 162 | "type": "object", 163 | "properties": { 164 | "isEnabled": { 165 | "type": "boolean" 166 | }, 167 | "pathname": { 168 | "type": "string" 169 | }, 170 | "robots": { 171 | "type": "object", 172 | "properties": { 173 | "index": { 174 | "type": "boolean" 175 | }, 176 | "follow": { 177 | "type": "boolean" 178 | } 179 | }, 180 | "required": ["index"] 181 | } 182 | }, 183 | "required": ["isEnabled", "pathname", "robots"] 184 | }, 185 | "category": { 186 | "type": "object", 187 | "properties": { 188 | "isEnabled": { 189 | "type": "boolean" 190 | }, 191 | "pathname": { 192 | "type": "string" 193 | }, 194 | "robots": { 195 | "type": "object", 196 | "properties": { 197 | "index": { 198 | "type": "boolean" 199 | }, 200 | "follow": { 201 | "type": "boolean" 202 | } 203 | }, 204 | "required": ["index"] 205 | } 206 | }, 207 | "required": ["isEnabled", "pathname", "robots"] 208 | }, 209 | "tag": { 210 | "type": "object", 211 | "properties": { 212 | "isEnabled": { 213 | "type": "boolean" 214 | }, 215 | "pathname": { 216 | "type": "string" 217 | }, 218 | "robots": { 219 | "type": "object", 220 | "properties": { 221 | "index": { 222 | "type": "boolean" 223 | }, 224 | "follow": { 225 | "type": "boolean" 226 | } 227 | }, 228 | "required": ["index"] 229 | } 230 | }, 231 | "required": ["isEnabled", "pathname", "robots"] 232 | } 233 | }, 234 | "required": ["isEnabled", "postsPerPage", "post", "list", "category", "tag"] 235 | } 236 | }, 237 | "required": ["blog"] 238 | }, 239 | "analytics": { 240 | "type": "object", 241 | "properties": { 242 | "vendors": { 243 | "type": "object", 244 | "properties": { 245 | "googleAnalytics": { 246 | "type": "object", 247 | "properties": { 248 | "id": { 249 | "type": ["string", "null"] 250 | }, 251 | "partytown": { 252 | "type": "boolean", 253 | "default": true 254 | } 255 | }, 256 | "required": ["id"] 257 | } 258 | }, 259 | "required": ["googleAnalytics"] 260 | } 261 | }, 262 | "required": ["vendors"] 263 | }, 264 | "ui": { 265 | "type": "object", 266 | "properties": { 267 | "theme": { 268 | "type": "string" 269 | } 270 | }, 271 | "required": ["theme"] 272 | } 273 | }, 274 | "required": ["site", "metadata", "i18n", "apps", "analytics", "ui"] 275 | } 276 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "astro-build.astro-vscode", 4 | "bradlc.vscode-tailwindcss", 5 | "dbaeumer.vscode-eslint", 6 | "esbenp.prettier-vscode", 7 | "unifiedjs.vscode-mdx" 8 | ], 9 | "unwantedRecommendations": [] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "css.customData": ["./vscode.tailwind.json"], 3 | "eslint.validate": ["javascript", "javascriptreact", "astro", "typescript", "typescriptreact"], 4 | "files.associations": { 5 | "*.mdx": "markdown" 6 | }, 7 | "prettier.documentSelectors": ["**/*.astro"], 8 | "[astro]": { 9 | "editor.defaultFormatter": "astro-build.astro-vscode" 10 | }, 11 | "yaml.schemas": { 12 | "./.vscode/astrowind/config-schema.json": "/src/config.yaml" 13 | }, 14 | "eslint.useFlatConfig": true 15 | } 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts AS base 2 | WORKDIR /app 3 | 4 | FROM base AS deps 5 | COPY package*.json ./ 6 | RUN npm install 7 | 8 | FROM base AS build 9 | COPY --from=deps /app/node_modules ./node_modules 10 | COPY . . 11 | RUN npm run build 12 | 13 | FROM nginx:stable-alpine AS deploy 14 | COPY --from=build /app/dist /usr/share/nginx/html 15 | COPY ./nginx/nginx.conf /etc/nginx/nginx.conf 16 | 17 | EXPOSE 8080 18 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 onWidget 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 | -------------------------------------------------------------------------------- /astro.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | 4 | import { defineConfig } from 'astro/config'; 5 | 6 | import sitemap from '@astrojs/sitemap'; 7 | import tailwind from '@astrojs/tailwind'; 8 | import mdx from '@astrojs/mdx'; 9 | import partytown from '@astrojs/partytown'; 10 | import icon from 'astro-icon'; 11 | import compress from 'astro-compress'; 12 | import type { AstroIntegration } from 'astro'; 13 | 14 | import astrowind from './vendor/integration'; 15 | 16 | import { readingTimeRemarkPlugin, responsiveTablesRehypePlugin, lazyImagesRehypePlugin } from './src/utils/frontmatter'; 17 | 18 | const __dirname = path.dirname(fileURLToPath(import.meta.url)); 19 | 20 | const hasExternalScripts = false; 21 | const whenExternalScripts = (items: (() => AstroIntegration) | (() => AstroIntegration)[] = []) => 22 | hasExternalScripts ? (Array.isArray(items) ? items.map((item) => item()) : [items()]) : []; 23 | 24 | export default defineConfig({ 25 | output: 'static', 26 | 27 | integrations: [ 28 | tailwind({ 29 | applyBaseStyles: false, 30 | }), 31 | sitemap(), 32 | mdx(), 33 | icon({ 34 | include: { 35 | tabler: ['*'], 36 | 'flat-color-icons': [ 37 | 'template', 38 | 'gallery', 39 | 'approval', 40 | 'document', 41 | 'advertising', 42 | 'currency-exchange', 43 | 'voice-presentation', 44 | 'business-contact', 45 | 'database', 46 | ], 47 | }, 48 | }), 49 | 50 | ...whenExternalScripts(() => 51 | partytown({ 52 | config: { forward: ['dataLayer.push'] }, 53 | }) 54 | ), 55 | 56 | compress({ 57 | CSS: true, 58 | HTML: { 59 | 'html-minifier-terser': { 60 | removeAttributeQuotes: false, 61 | }, 62 | }, 63 | Image: false, 64 | JavaScript: true, 65 | SVG: false, 66 | Logger: 1, 67 | }), 68 | 69 | astrowind({ 70 | config: './src/config.yaml', 71 | }), 72 | ], 73 | 74 | image: { 75 | domains: ['cdn.pixabay.com'], 76 | }, 77 | 78 | markdown: { 79 | remarkPlugins: [readingTimeRemarkPlugin], 80 | rehypePlugins: [responsiveTablesRehypePlugin, lazyImagesRehypePlugin], 81 | }, 82 | 83 | vite: { 84 | resolve: { 85 | alias: { 86 | '~': path.resolve(__dirname, './src'), 87 | }, 88 | }, 89 | }, 90 | }); 91 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | astrowind: 3 | build: . 4 | container_name: astrowind 5 | ports: 6 | - 8080:8080 7 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import astroEslintParser from 'astro-eslint-parser'; 2 | import eslintPluginAstro from 'eslint-plugin-astro'; 3 | import globals from 'globals'; 4 | import js from '@eslint/js'; 5 | import tseslint from 'typescript-eslint'; 6 | import typescriptParser from '@typescript-eslint/parser'; 7 | 8 | export default [ 9 | js.configs.recommended, 10 | ...eslintPluginAstro.configs['flat/recommended'], 11 | ...tseslint.configs.recommended, 12 | { 13 | languageOptions: { 14 | globals: { 15 | ...globals.browser, 16 | ...globals.node, 17 | }, 18 | }, 19 | }, 20 | { 21 | files: ['**/*.astro'], 22 | languageOptions: { 23 | parser: astroEslintParser, 24 | parserOptions: { 25 | parser: '@typescript-eslint/parser', 26 | extraFileExtensions: ['.astro'], 27 | }, 28 | }, 29 | }, 30 | { 31 | files: ['**/*.{js,jsx,astro}'], 32 | rules: { 33 | 'no-mixed-spaces-and-tabs': ['error', 'smart-tabs'], 34 | }, 35 | }, 36 | { 37 | // Define the configuration for ` 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: -------------------------------------------------------------------------------- /sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "infiniteLoopProtection": true, 3 | "hardReloadOnChange": false, 4 | "view": "browser", 5 | "template": "node", 6 | "container": { 7 | "port": 3000, 8 | "startScript": "start", 9 | "node": "18" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/assets/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onwidget/astrowind/76f8788ef1575a4482640ff3a9dfd3b0d364be24/src/assets/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /src/assets/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onwidget/astrowind/76f8788ef1575a4482640ff3a9dfd3b0d364be24/src/assets/favicons/favicon.ico -------------------------------------------------------------------------------- /src/assets/favicons/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/images/app-store.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onwidget/astrowind/76f8788ef1575a4482640ff3a9dfd3b0d364be24/src/assets/images/app-store.png -------------------------------------------------------------------------------- /src/assets/images/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onwidget/astrowind/76f8788ef1575a4482640ff3a9dfd3b0d364be24/src/assets/images/default.png -------------------------------------------------------------------------------- /src/assets/images/google-play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onwidget/astrowind/76f8788ef1575a4482640ff3a9dfd3b0d364be24/src/assets/images/google-play.png -------------------------------------------------------------------------------- /src/assets/images/hero-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/onwidget/astrowind/76f8788ef1575a4482640ff3a9dfd3b0d364be24/src/assets/images/hero-image.png -------------------------------------------------------------------------------- /src/assets/styles/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer utilities { 6 | .bg-page { 7 | background-color: var(--aw-color-bg-page); 8 | } 9 | .bg-dark { 10 | background-color: var(--aw-color-bg-page-dark); 11 | } 12 | .bg-light { 13 | background-color: var(--aw-color-bg-page); 14 | } 15 | .text-page { 16 | color: var(--aw-color-text-page); 17 | } 18 | .text-muted { 19 | color: var(--aw-color-text-muted); 20 | } 21 | } 22 | 23 | @layer components { 24 | .btn { 25 | @apply inline-flex items-center justify-center rounded-full border-gray-400 border bg-transparent font-medium text-center text-base text-page leading-snug transition py-3.5 px-6 md:px-8 ease-in duration-200 focus:ring-blue-500 focus:ring-offset-blue-200 focus:ring-2 focus:ring-offset-2 hover:bg-gray-100 hover:border-gray-600 dark:text-slate-300 dark:border-slate-500 dark:hover:bg-slate-800 dark:hover:border-slate-800 cursor-pointer; 26 | } 27 | 28 | .btn-primary { 29 | @apply btn font-semibold bg-primary text-white border-primary hover:bg-secondary hover:border-secondary hover:text-white dark:text-white dark:bg-primary dark:border-primary dark:hover:border-secondary dark:hover:bg-secondary; 30 | } 31 | 32 | .btn-secondary { 33 | @apply btn; 34 | } 35 | 36 | .btn-tertiary { 37 | @apply btn border-none shadow-none text-muted hover:text-gray-900 dark:text-gray-400 dark:hover:text-white; 38 | } 39 | } 40 | 41 | #header.scroll > div:first-child { 42 | @apply bg-page md:bg-white/90 md:backdrop-blur-md; 43 | box-shadow: 0 0.375rem 1.5rem 0 rgb(140 152 164 / 13%); 44 | } 45 | .dark #header.scroll > div:first-child, 46 | #header.scroll.dark > div:first-child { 47 | @apply bg-page md:bg-[#030621e6] border-b border-gray-500/20; 48 | box-shadow: none; 49 | } 50 | /* #header.scroll > div:last-child { 51 | @apply py-3; 52 | } */ 53 | 54 | #header.expanded nav { 55 | position: fixed; 56 | top: 70px; 57 | left: 0; 58 | right: 0; 59 | bottom: 70px !important; 60 | padding: 0 5px; 61 | } 62 | 63 | .dropdown:focus .dropdown-menu, 64 | .dropdown:focus-within .dropdown-menu, 65 | .dropdown:hover .dropdown-menu { 66 | display: block; 67 | } 68 | 69 | [astro-icon].icon-light > * { 70 | stroke-width: 1.2; 71 | } 72 | 73 | [astro-icon].icon-bold > * { 74 | stroke-width: 2.4; 75 | } 76 | 77 | [data-aw-toggle-menu] path { 78 | @apply transition; 79 | } 80 | [data-aw-toggle-menu].expanded g > path:first-child { 81 | @apply -rotate-45 translate-y-[15px] translate-x-[-3px]; 82 | } 83 | 84 | [data-aw-toggle-menu].expanded g > path:last-child { 85 | @apply rotate-45 translate-y-[-8px] translate-x-[14px]; 86 | } 87 | 88 | /* To deprecated */ 89 | 90 | .dd *:first-child { 91 | margin-top: 0; 92 | } 93 | -------------------------------------------------------------------------------- /src/components/CustomStyles.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import '@fontsource-variable/inter'; 3 | 4 | // 'DM Sans' 5 | // Nunito 6 | // Dosis 7 | // Outfit 8 | // Roboto 9 | // Literata 10 | // 'IBM Plex Sans' 11 | // Karla 12 | // Poppins 13 | // 'Fira Sans' 14 | // 'Libre Franklin' 15 | // Inconsolata 16 | // Raleway 17 | // Oswald 18 | // 'Space Grotesk' 19 | // Urbanist 20 | --- 21 | 22 | 64 | -------------------------------------------------------------------------------- /src/components/Favicons.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import favIcon from '~/assets/favicons/favicon.ico'; 3 | import favIconSvg from '~/assets/favicons/favicon.svg'; 4 | import appleTouchIcon from '~/assets/favicons/apple-touch-icon.png'; 5 | --- 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/Logo.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { SITE } from 'astrowind:config'; 3 | --- 4 | 5 | 8 | 🚀 {SITE?.name} 9 | 10 | -------------------------------------------------------------------------------- /src/components/blog/Grid.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Item from '~/components/blog/GridItem.astro'; 3 | import type { Post } from '~/types'; 4 | 5 | export interface Props { 6 | posts: Array; 7 | } 8 | 9 | const { posts } = Astro.props; 10 | --- 11 | 12 |
13 | {posts.map((post) => )} 14 |
15 | -------------------------------------------------------------------------------- /src/components/blog/GridItem.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { APP_BLOG } from 'astrowind:config'; 3 | import type { Post } from '~/types'; 4 | 5 | import Image from '~/components/common/Image.astro'; 6 | 7 | import { findImage } from '~/utils/images'; 8 | import { getPermalink } from '~/utils/permalinks'; 9 | 10 | export interface Props { 11 | post: Post; 12 | } 13 | 14 | const { post } = Astro.props; 15 | const image = await findImage(post.image); 16 | 17 | const link = APP_BLOG?.post?.isEnabled ? getPermalink(post.permalink, 'post') : ''; 18 | --- 19 | 20 |
23 |
24 | { 25 | image && 26 | (link ? ( 27 | 28 | {post.title} 40 | 41 | ) : ( 42 | {post.title} 54 | )) 55 | } 56 |
57 | 58 |

59 | { 60 | link ? ( 61 | 62 | {post.title} 63 | 64 | ) : ( 65 | post.title 66 | ) 67 | } 68 |

69 | 70 |

{post.excerpt}

71 |
72 | -------------------------------------------------------------------------------- /src/components/blog/Headline.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const { title = await Astro.slots.render('default'), subtitle = await Astro.slots.render('subtitle') } = Astro.props; 3 | --- 4 | 5 |
6 |

7 | { 8 | subtitle && ( 9 |
10 | ) 11 | } 12 |

13 | -------------------------------------------------------------------------------- /src/components/blog/List.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Item from '~/components/blog/ListItem.astro'; 3 | import type { Post } from '~/types'; 4 | 5 | export interface Props { 6 | posts: Array; 7 | } 8 | 9 | const { posts } = Astro.props; 10 | --- 11 | 12 | 21 | -------------------------------------------------------------------------------- /src/components/blog/ListItem.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { ImageMetadata } from 'astro'; 3 | import { Icon } from 'astro-icon/components'; 4 | import Image from '~/components/common/Image.astro'; 5 | import PostTags from '~/components/blog/Tags.astro'; 6 | 7 | import { APP_BLOG } from 'astrowind:config'; 8 | import type { Post } from '~/types'; 9 | 10 | import { getPermalink } from '~/utils/permalinks'; 11 | import { findImage } from '~/utils/images'; 12 | import { getFormattedDate } from '~/utils/utils'; 13 | 14 | export interface Props { 15 | post: Post; 16 | } 17 | 18 | const { post } = Astro.props; 19 | const image = (await findImage(post.image)) as ImageMetadata | undefined; 20 | 21 | const link = APP_BLOG?.post?.isEnabled ? getPermalink(post.permalink, 'post') : ''; 22 | --- 23 | 24 |
27 | { 28 | image && 29 | (link ? ( 30 | 31 | 46 | 47 | ) : ( 48 | 63 | )) 64 | } 65 |
66 |
67 |
68 | 69 | 70 | 71 | { 72 | post.author && ( 73 | <> 74 | {' '} 75 | · 76 | {post.author.replaceAll('-', ' ')} 77 | 78 | ) 79 | } 80 | { 81 | post.category && ( 82 | <> 83 | {' '} 84 | ·{' '} 85 | 86 | {post.category.title} 87 | 88 | 89 | ) 90 | } 91 | 92 |
93 |

94 | { 95 | link ? ( 96 | 100 | {post.title} 101 | 102 | ) : ( 103 | post.title 104 | ) 105 | } 106 |

107 |
108 | 109 | {post.excerpt &&

{post.excerpt}

} 110 | { 111 | post.tags && Array.isArray(post.tags) ? ( 112 |
113 | 114 |
115 | ) : ( 116 | 117 | ) 118 | } 119 |
120 |
121 | -------------------------------------------------------------------------------- /src/components/blog/Pagination.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Icon } from 'astro-icon/components'; 3 | import { getPermalink } from '~/utils/permalinks'; 4 | import Button from '~/components/ui/Button.astro'; 5 | 6 | export interface Props { 7 | prevUrl?: string; 8 | nextUrl?: string; 9 | prevText?: string; 10 | nextText?: string; 11 | } 12 | 13 | const { prevUrl, nextUrl, prevText = 'Newer posts', nextText = 'Older posts' } = Astro.props; 14 | --- 15 | 16 | { 17 | (prevUrl || nextUrl) && ( 18 |
19 |
20 | 28 | 29 | 33 |
34 |
35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /src/components/blog/RelatedPosts.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { APP_BLOG } from 'astrowind:config'; 3 | 4 | import { getRelatedPosts } from '~/utils/blog'; 5 | import BlogHighlightedPosts from '../widgets/BlogHighlightedPosts.astro'; 6 | import type { Post } from '~/types'; 7 | import { getBlogPermalink } from '~/utils/permalinks'; 8 | 9 | export interface Props { 10 | post: Post; 11 | } 12 | 13 | const { post } = Astro.props; 14 | 15 | const relatedPosts = post.tags ? await getRelatedPosts(post, 4) : []; 16 | --- 17 | 18 | { 19 | APP_BLOG.isRelatedPostsEnabled ? ( 20 | post.id)} 29 | /> 30 | ) : null 31 | } 32 | -------------------------------------------------------------------------------- /src/components/blog/SinglePost.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Icon } from 'astro-icon/components'; 3 | 4 | import Image from '~/components/common/Image.astro'; 5 | import PostTags from '~/components/blog/Tags.astro'; 6 | import SocialShare from '~/components/common/SocialShare.astro'; 7 | 8 | import { getPermalink } from '~/utils/permalinks'; 9 | import { getFormattedDate } from '~/utils/utils'; 10 | 11 | import type { Post } from '~/types'; 12 | 13 | export interface Props { 14 | post: Post; 15 | url: string | URL; 16 | } 17 | 18 | const { post, url } = Astro.props; 19 | --- 20 | 21 |
22 |
23 |
28 |
29 |

30 | 31 | 32 | { 33 | post.author && ( 34 | <> 35 | {' '} 36 | · 37 | {post.author} 38 | 39 | ) 40 | } 41 | { 42 | post.category && ( 43 | <> 44 | {' '} 45 | ·{' '} 46 | 47 | {post.category.title} 48 | 49 | 50 | ) 51 | } 52 | { 53 | post.readingTime && ( 54 | <> 55 |  · {post.readingTime} min read 56 | 57 | ) 58 | } 59 |

60 |
61 | 62 |

65 | {post.title} 66 |

67 |

70 | {post.excerpt} 71 |

72 | 73 | { 74 | post.image ? ( 75 | {post?.excerpt 86 | ) : ( 87 |
88 |
89 |
90 | ) 91 | } 92 |
93 |
96 | 97 |
98 |
99 | 100 | 101 |
102 |
103 |
104 | -------------------------------------------------------------------------------- /src/components/blog/Tags.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getPermalink } from '~/utils/permalinks'; 3 | 4 | import { APP_BLOG } from 'astrowind:config'; 5 | import type { Post } from '~/types'; 6 | 7 | export interface Props { 8 | tags: Post['tags']; 9 | class?: string; 10 | title?: string | undefined; 11 | isCategory?: boolean; 12 | } 13 | 14 | const { tags, class: className = 'text-sm', title = undefined, isCategory = false } = Astro.props; 15 | --- 16 | 17 | { 18 | tags && Array.isArray(tags) && ( 19 | <> 20 | {title !== undefined && ( 21 | 22 | {title} 23 | 24 | )} 25 |
    26 | {tags.map((tag) => ( 27 |
  • 28 | {!APP_BLOG?.tag?.isEnabled ? ( 29 | tag.title 30 | ) : ( 31 | 35 | {tag.title} 36 | 37 | )} 38 |
  • 39 | ))} 40 |
41 | 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /src/components/blog/ToBlogLink.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Icon } from 'astro-icon/components'; 3 | import { getBlogPermalink } from '~/utils/permalinks'; 4 | import { I18N } from 'astrowind:config'; 5 | import Button from '~/components/ui/Button.astro'; 6 | 7 | const { textDirection } = I18N; 8 | --- 9 | 10 |
11 | 20 |
21 | -------------------------------------------------------------------------------- /src/components/common/Analytics.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { GoogleAnalytics } from '@astrolib/analytics'; 3 | import { ANALYTICS } from 'astrowind:config'; 4 | --- 5 | 6 | { 7 | ANALYTICS?.vendors?.googleAnalytics?.id ? ( 8 | 12 | ) : null 13 | } 14 | -------------------------------------------------------------------------------- /src/components/common/ApplyColorMode.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { UI } from 'astrowind:config'; 3 | 4 | // TODO: This code is temporary 5 | --- 6 | 7 | 34 | -------------------------------------------------------------------------------- /src/components/common/CommonMeta.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { getAsset } from '~/utils/permalinks'; 3 | --- 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/components/common/Image.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { HTMLAttributes } from 'astro/types'; 3 | import { findImage } from '~/utils/images'; 4 | import { 5 | getImagesOptimized, 6 | astroAssetsOptimizer, 7 | unpicOptimizer, 8 | isUnpicCompatible, 9 | type ImageProps, 10 | } from '~/utils/images-optimization'; 11 | 12 | type Props = ImageProps; 13 | type ImageType = { 14 | src: string; 15 | attributes: HTMLAttributes<'img'>; 16 | }; 17 | 18 | const props = Astro.props; 19 | 20 | if (props.alt === undefined || props.alt === null) { 21 | throw new Error(); 22 | } 23 | 24 | if (typeof props.width === 'string') { 25 | props.width = parseInt(props.width); 26 | } 27 | 28 | if (typeof props.height === 'string') { 29 | props.height = parseInt(props.height); 30 | } 31 | 32 | if (!props.loading) { 33 | props.loading = 'lazy'; 34 | } 35 | 36 | if (!props.decoding) { 37 | props.decoding = 'async'; 38 | } 39 | 40 | const _image = await findImage(props.src); 41 | 42 | let image: ImageType | undefined = undefined; 43 | 44 | if ( 45 | typeof _image === 'string' && 46 | (_image.startsWith('http://') || _image.startsWith('https://')) && 47 | isUnpicCompatible(_image) 48 | ) { 49 | image = await getImagesOptimized(_image, props, unpicOptimizer); 50 | } else if (_image) { 51 | image = await getImagesOptimized(_image, props, astroAssetsOptimizer); 52 | } 53 | --- 54 | 55 | { 56 | !image ? ( 57 | 58 | ) : ( 59 | 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /src/components/common/Metadata.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import merge from 'lodash.merge'; 3 | import { AstroSeo } from '@astrolib/seo'; 4 | 5 | import type { Props as AstroSeoProps } from '@astrolib/seo'; 6 | 7 | import { SITE, METADATA, I18N } from 'astrowind:config'; 8 | import type { MetaData } from '~/types'; 9 | import { getCanonical } from '~/utils/permalinks'; 10 | 11 | import { adaptOpenGraphImages } from '~/utils/images'; 12 | 13 | export interface Props extends MetaData { 14 | dontUseTitleTemplate?: boolean; 15 | } 16 | 17 | const { 18 | title, 19 | ignoreTitleTemplate = false, 20 | canonical = String(getCanonical(String(Astro.url.pathname))), 21 | robots = {}, 22 | description, 23 | openGraph = {}, 24 | twitter = {}, 25 | } = Astro.props; 26 | 27 | const seoProps: AstroSeoProps = merge( 28 | { 29 | title: '', 30 | titleTemplate: '%s', 31 | canonical: canonical, 32 | noindex: true, 33 | nofollow: true, 34 | description: undefined, 35 | openGraph: { 36 | url: canonical, 37 | site_name: SITE?.name, 38 | images: [], 39 | locale: I18N?.language || 'en', 40 | type: 'website', 41 | }, 42 | twitter: { 43 | cardType: openGraph?.images?.length ? 'summary_large_image' : 'summary', 44 | }, 45 | }, 46 | { 47 | title: METADATA?.title?.default, 48 | titleTemplate: METADATA?.title?.template, 49 | noindex: typeof METADATA?.robots?.index !== 'undefined' ? !METADATA.robots.index : undefined, 50 | nofollow: typeof METADATA?.robots?.follow !== 'undefined' ? !METADATA.robots.follow : undefined, 51 | description: METADATA?.description, 52 | openGraph: METADATA?.openGraph, 53 | twitter: METADATA?.twitter, 54 | }, 55 | { 56 | title: title, 57 | titleTemplate: ignoreTitleTemplate ? '%s' : undefined, 58 | canonical: canonical, 59 | noindex: typeof robots?.index !== 'undefined' ? !robots.index : undefined, 60 | nofollow: typeof robots?.follow !== 'undefined' ? !robots.follow : undefined, 61 | description: description, 62 | openGraph: { url: canonical, ...openGraph }, 63 | twitter: twitter, 64 | } 65 | ); 66 | --- 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/components/common/SiteVerification.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { SITE } from 'astrowind:config'; 3 | --- 4 | 5 | {SITE.googleSiteVerificationId && } 6 | -------------------------------------------------------------------------------- /src/components/common/SocialShare.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Icon } from 'astro-icon/components'; 3 | 4 | export interface Props { 5 | text: string; 6 | url: string | URL; 7 | class?: string; 8 | } 9 | 10 | const { text, url, class: className = 'inline-block' } = Astro.props; 11 | --- 12 | 13 |
14 | Share: 15 | 26 | 32 | 43 | 54 | 65 |
66 | -------------------------------------------------------------------------------- /src/components/common/SplitbeeAnalytics.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const { doNotTrack = true, noCookieMode = false, url = 'https://cdn.splitbee.io/sb.js' } = Astro.props; 3 | --- 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/common/ToggleMenu.astro: -------------------------------------------------------------------------------- 1 | --- 2 | export interface Props { 3 | label?: string; 4 | class?: string; 5 | } 6 | 7 | const { 8 | label = 'Toggle Menu', 9 | class: className = 'flex flex-col h-12 w-12 rounded justify-center items-center cursor-pointer group', 10 | } = Astro.props; 11 | --- 12 | 13 | 30 | -------------------------------------------------------------------------------- /src/components/common/ToggleTheme.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Icon } from 'astro-icon/components'; 3 | 4 | import { UI } from 'astrowind:config'; 5 | 6 | export interface Props { 7 | label?: string; 8 | class?: string; 9 | iconClass?: string; 10 | iconName?: string; 11 | } 12 | 13 | const { 14 | label = 'Toggle between Dark and Light mode', 15 | class: 16 | className = 'text-muted dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none focus:ring-4 focus:ring-gray-200 dark:focus:ring-gray-700 rounded-lg text-sm p-2.5 inline-flex items-center', 17 | iconClass = 'w-6 h-6', 18 | iconName = 'tabler:sun', 19 | } = Astro.props; 20 | --- 21 | 22 | { 23 | !(UI.theme && UI.theme.endsWith(':only')) && ( 24 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /src/components/ui/Background.astro: -------------------------------------------------------------------------------- 1 | --- 2 | export interface Props { 3 | isDark?: boolean; 4 | } 5 | 6 | const { isDark = false } = Astro.props; 7 | --- 8 | 9 |
10 | 11 |
12 | -------------------------------------------------------------------------------- /src/components/ui/Button.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Icon } from 'astro-icon/components'; 3 | import { twMerge } from 'tailwind-merge'; 4 | import type { CallToAction as Props } from '~/types'; 5 | 6 | const { 7 | variant = 'secondary', 8 | target, 9 | text = Astro.slots.render('default'), 10 | icon = '', 11 | class: className = '', 12 | type, 13 | ...rest 14 | } = Astro.props; 15 | 16 | const variants = { 17 | primary: 'btn-primary', 18 | secondary: 'btn-secondary', 19 | tertiary: 'btn btn-tertiary', 20 | link: 'cursor-pointer hover:text-primary', 21 | }; 22 | --- 23 | 24 | { 25 | type === 'button' || type === 'submit' || type === 'reset' ? ( 26 | 30 | ) : ( 31 | 36 | 37 | {icon && } 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/components/ui/DListItem.astro: -------------------------------------------------------------------------------- 1 | --- 2 | // component: DListItem 3 | // 4 | // Mimics the html 'dl' (description list) 5 | // 6 | // The 'dt' item is the item 'term' and is inserted into an 'h6' tag. 7 | // Caller needs to style the 'h6' tag appropriately. 8 | // 9 | // You can put pretty much any content you want between the open and 10 | // closing tags - it's simply contained in an enclosing div with a 11 | // margin left. No need for 'dd' items. 12 | // 13 | const { dt } = Astro.props; 14 | interface Props { 15 | dt: string; 16 | } 17 | 18 | const content: string = await Astro.slots.render('default'); 19 | --- 20 | 21 |
22 |
23 | -------------------------------------------------------------------------------- /src/components/ui/Form.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import type { Form as Props } from '~/types'; 3 | import Button from '~/components/ui/Button.astro'; 4 | 5 | const { inputs, textarea, disclaimer, button = 'Contact us', description = '' } = Astro.props; 6 | --- 7 | 8 |
9 | { 10 | inputs && 11 | inputs.map( 12 | ({ type = 'text', name, label = '', autocomplete = 'on', placeholder = '' }) => 13 | name && ( 14 |
15 | {label && ( 16 | 19 | )} 20 | 28 |
29 | ) 30 | ) 31 | } 32 | 33 | { 34 | textarea && ( 35 |
36 | 39 |