initialize with https://github.com/onwidget/astrowind
12
v3/.editorconfig
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# https://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
indent_size = 2
|
||||||
|
indent_style = space
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = false
|
41
v3/.github/workflows/actions.yaml
vendored
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
name: GitHub Actions
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version:
|
||||||
|
- 18
|
||||||
|
- 20
|
||||||
|
- 22
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Use Node.js v${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: npm
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run build
|
||||||
|
# - run: npm test
|
||||||
|
|
||||||
|
check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Use Node.js 22
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: npm
|
||||||
|
- run: npm ci
|
||||||
|
- run: npm run check
|
24
v3/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# build output
|
||||||
|
dist/
|
||||||
|
.output/
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# logs
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
|
||||||
|
# environment variables
|
||||||
|
.env
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# macOS-specific files
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
pnpm-lock.yaml
|
||||||
|
|
||||||
|
.astro
|
2
v3/.npmrc
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Expose Astro dependencies for `pnpm` users
|
||||||
|
shamefully-hoist=true
|
4
v3/.prettierignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
.github
|
||||||
|
.changeset
|
13
v3/.prettierrc.cjs
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
/** @type {import('prettier').Config} */
|
||||||
|
module.exports = {
|
||||||
|
printWidth: 120,
|
||||||
|
semi: true,
|
||||||
|
singleQuote: true,
|
||||||
|
tabWidth: 2,
|
||||||
|
trailingComma: 'es5',
|
||||||
|
useTabs: false,
|
||||||
|
|
||||||
|
plugins: [require.resolve('prettier-plugin-astro')],
|
||||||
|
|
||||||
|
overrides: [{ files: '*.astro', options: { parser: 'astro' } }],
|
||||||
|
};
|
6
v3/.stackblitzrc
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"startCommand": "npm start",
|
||||||
|
"env": {
|
||||||
|
"ENABLE_CJS_IMPORTS": true
|
||||||
|
}
|
||||||
|
}
|
275
v3/.vscode/astrowind/config-schema.json
vendored
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"site": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"site": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"base": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"trailingSlash": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"googleSiteVerificationId": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["name", "site", "base", "trailingSlash"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"title": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"default": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"template": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["default", "template"]
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"robots": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"index": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"follow": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["index", "follow"]
|
||||||
|
},
|
||||||
|
"openGraph": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"site_name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"images": {
|
||||||
|
"type": "array",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"width": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"height": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["url", "width", "height"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["site_name", "images", "type"]
|
||||||
|
},
|
||||||
|
"twitter": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"handle": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"site": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"cardType": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["handle", "site", "cardType"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["title", "description", "robots", "openGraph", "twitter"]
|
||||||
|
},
|
||||||
|
"i18n": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"language": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"textDirection": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["language", "textDirection"]
|
||||||
|
},
|
||||||
|
"apps": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"blog": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"isEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"postsPerPage": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"isRelatedPostsEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"relatedPostsCount": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"isEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"permalink": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"robots": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"index": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"follow": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["index"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["isEnabled", "permalink", "robots"]
|
||||||
|
},
|
||||||
|
"list": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"isEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"pathname": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"robots": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"index": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"follow": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["index"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["isEnabled", "pathname", "robots"]
|
||||||
|
},
|
||||||
|
"category": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"isEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"pathname": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"robots": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"index": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"follow": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["index"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["isEnabled", "pathname", "robots"]
|
||||||
|
},
|
||||||
|
"tag": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"isEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"pathname": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"robots": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"index": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"follow": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["index"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["isEnabled", "pathname", "robots"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["isEnabled", "postsPerPage", "post", "list", "category", "tag"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["blog"]
|
||||||
|
},
|
||||||
|
"analytics": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"vendors": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"googleAnalytics": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": ["string", "null"]
|
||||||
|
},
|
||||||
|
"partytown": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["id"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["googleAnalytics"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["vendors"]
|
||||||
|
},
|
||||||
|
"ui": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"theme": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["theme"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["site", "metadata", "i18n", "apps", "analytics", "ui"]
|
||||||
|
}
|
10
v3/.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"astro-build.astro-vscode",
|
||||||
|
"bradlc.vscode-tailwindcss",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"unifiedjs.vscode-mdx"
|
||||||
|
],
|
||||||
|
"unwantedRecommendations": []
|
||||||
|
}
|
11
v3/.vscode/launch.json
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"command": "./node_modules/.bin/astro dev",
|
||||||
|
"name": "Development server",
|
||||||
|
"request": "launch",
|
||||||
|
"type": "node-terminal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
15
v3/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"css.customData": ["./vscode.tailwind.json"],
|
||||||
|
"eslint.validate": ["javascript", "javascriptreact", "astro", "typescript", "typescriptreact"],
|
||||||
|
"files.associations": {
|
||||||
|
"*.mdx": "markdown"
|
||||||
|
},
|
||||||
|
"prettier.documentSelectors": ["**/*.astro"],
|
||||||
|
"[astro]": {
|
||||||
|
"editor.defaultFormatter": "astro-build.astro-vscode"
|
||||||
|
},
|
||||||
|
"yaml.schemas": {
|
||||||
|
"./.vscode/astrowind/config-schema.json": "/src/config.yaml"
|
||||||
|
},
|
||||||
|
"eslint.useFlatConfig": true
|
||||||
|
}
|
21
v3/LICENSE.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 onWidget
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
295
v3/README.md
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
# 🚀 AstroWind
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/onwidget/.github/main/resources/astrowind/lighthouse-score.png" align="right"
|
||||||
|
alt="AstroWind Lighthouse Score" width="100" height="358">
|
||||||
|
|
||||||
|
🌟 _Most *starred* & *forked* Astro theme in 2022 & 2023_. 🌟
|
||||||
|
|
||||||
|
**AstroWind** is a free and open-source template to make your website using **[Astro 4.0](https://astro.build/) + [Tailwind CSS](https://tailwindcss.com/)**. Ready to start a new project and designed taking into account web best practices.
|
||||||
|
|
||||||
|
- ✅ **Production-ready** scores in **PageSpeed Insights** reports.
|
||||||
|
- ✅ Integration with **Tailwind CSS** supporting **Dark mode** and **_RTL_**.
|
||||||
|
- ✅ **Fast and SEO friendly blog** with automatic **RSS feed**, **MDX** support, **Categories & Tags**, **Social Share**, ...
|
||||||
|
- ✅ **Image Optimization** (using new **Astro Assets** and **Unpic** for Universal image CDN).
|
||||||
|
- ✅ Generation of **project sitemap** based on your routes.
|
||||||
|
- ✅ **Open Graph tags** for social media sharing.
|
||||||
|
- ✅ **Analytics** built-in Google Analytics, and Splitbee integration.
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<img src="https://raw.githubusercontent.com/onwidget/.github/main/resources/astrowind/screenshot-astrowind-1.png" alt="AstroWind Theme Screenshot">
|
||||||
|
|
||||||
|
[![onWidget](https://custom-icon-badges.demolab.com/badge/made%20by%20-onWidget-556bf2?style=flat-square&logo=onwidget&logoColor=white&labelColor=101827)](https://onwidget.com)
|
||||||
|
[![License](https://img.shields.io/github/license/onwidget/astrowind?style=flat-square&color=dddddd&labelColor=000000)](https://github.com/onwidget/astrowind/blob/main/LICENSE.md)
|
||||||
|
[![Maintained](https://img.shields.io/badge/maintained%3F-yes-brightgreen.svg?style=flat-square)](https://github.com/onwidget)
|
||||||
|
[![Contributions Welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat-square)](https://github.com/onwidget/astrowind#contributing)
|
||||||
|
[![Known Vulnerabilities](https://snyk.io/test/github/onwidget/astrowind/badge.svg?style=flat-square)](https://snyk.io/test/github/onwidget/astrowind)
|
||||||
|
[![Stars](https://img.shields.io/github/stars/onwidget/astrowind.svg?style=social&label=stars&maxAge=86400&color=ff69b4)](https://github.com/onwidget/astrowind)
|
||||||
|
[![Forks](https://img.shields.io/github/forks/onwidget/astrowind.svg?style=social&label=forks&maxAge=86400&color=ff69b4)](https://github.com/onwidget/astrowind)
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<details open>
|
||||||
|
<summary>Table of Contents</summary>
|
||||||
|
|
||||||
|
- [Demo](#demo)
|
||||||
|
- [Upcoming: AstroWind 2.0 – We Need Your Vision!](#-upcoming-astrowind-20--we-need-your-vision)
|
||||||
|
- [Getting started](#getting-started)
|
||||||
|
- [Project structure](#project-structure)
|
||||||
|
- [Commands](#commands)
|
||||||
|
- [Configuration](#configuration)
|
||||||
|
- [Deploy](#deploy)
|
||||||
|
- [Frequently Asked Questions](#frequently-asked-questions)
|
||||||
|
- [Related Projects](#related-projects)
|
||||||
|
- [Contributing](#contributing)
|
||||||
|
- [Acknowledgements](#acknowledgements)
|
||||||
|
- [License](#license)
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
📌 [https://astrowind.vercel.app/](https://astrowind.vercel.app/)
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## 🔔 Upcoming: AstroWind 2.0 – We Need Your Vision!
|
||||||
|
|
||||||
|
We're embarking on an exciting journey with **AstroWind 2.0**, and we want you to be a part of it! We're currently taking the first steps in developing this new version and your insights are invaluable. Join the discussion and share your feedback, ideas, and suggestions to help shape the future of **AstroWind**. Let's make **AstroWind 2.0** even better, together!
|
||||||
|
|
||||||
|
[Share Your Feedback in Our Discussion!](https://github.com/onwidget/astrowind/discussions/392)
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
**AstroWind** tries to give you quick access to creating a website using [Astro 4.0](https://astro.build/) + [Tailwind CSS](https://tailwindcss.com/). It's a free theme which focuses on simplicity, good practices and high performance.
|
||||||
|
|
||||||
|
Very little vanilla javascript is used only to provide basic functionality so that each developer decides which framework (React, Vue, Svelte, Solid JS...) to use and how to approach their goals.
|
||||||
|
|
||||||
|
In this version the template supports all the options in the `output` configuration, `static`, `hybrid` and `server`, but the blog only works with `prerender = true`. We are working on the next version and aim to make it fully compatible with SSR.
|
||||||
|
|
||||||
|
### Project structure
|
||||||
|
|
||||||
|
Inside **AstroWind** template, you'll see the following folders and files:
|
||||||
|
|
||||||
|
```
|
||||||
|
/
|
||||||
|
├── public/
|
||||||
|
│ ├── _headers
|
||||||
|
│ └── robots.txt
|
||||||
|
├── src/
|
||||||
|
│ ├── assets/
|
||||||
|
│ │ ├── favicons/
|
||||||
|
│ │ ├── images/
|
||||||
|
│ │ └── styles/
|
||||||
|
│ │ └── tailwind.css
|
||||||
|
│ ├── components/
|
||||||
|
│ │ ├── blog/
|
||||||
|
│ │ ├── common/
|
||||||
|
│ │ ├── ui/
|
||||||
|
│ │ ├── widgets/
|
||||||
|
│ │ │ ├── Header.astro
|
||||||
|
│ │ │ └── ...
|
||||||
|
│ │ ├── CustomStyles.astro
|
||||||
|
│ │ ├── Favicons.astro
|
||||||
|
│ │ └── Logo.astro
|
||||||
|
│ ├── content/
|
||||||
|
│ │ ├── post/
|
||||||
|
│ │ │ ├── post-slug-1.md
|
||||||
|
│ │ │ ├── post-slug-2.mdx
|
||||||
|
│ │ │ └── ...
|
||||||
|
│ │ └-- config.ts
|
||||||
|
│ ├── layouts/
|
||||||
|
│ │ ├── Layout.astro
|
||||||
|
│ │ ├── MarkdownLayout.astro
|
||||||
|
│ │ └── PageLayout.astro
|
||||||
|
│ ├── pages/
|
||||||
|
│ │ ├── [...blog]/
|
||||||
|
│ │ │ ├── [category]/
|
||||||
|
│ │ │ ├── [tag]/
|
||||||
|
│ │ │ ├── [...page].astro
|
||||||
|
│ │ │ └── index.astro
|
||||||
|
│ │ ├── index.astro
|
||||||
|
│ │ ├── 404.astro
|
||||||
|
│ │ ├-- rss.xml.ts
|
||||||
|
│ │ └── ...
|
||||||
|
│ ├── utils/
|
||||||
|
│ ├── config.yaml
|
||||||
|
│ └── navigation.js
|
||||||
|
├── package.json
|
||||||
|
├── astro.config.ts
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
Astro looks for `.astro` or `.md` files in the `src/pages/` directory. Each page is exposed as a route based on its file name.
|
||||||
|
|
||||||
|
There's nothing special about `src/components/`, but that's where we like to put any Astro/React/Vue/Svelte/Preact components.
|
||||||
|
|
||||||
|
Any static assets, like images, can be placed in the `public/` directory if they do not require any transformation or in the `assets/` directory if they are imported directly.
|
||||||
|
|
||||||
|
[![Edit AstroWind on CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://githubbox.com/onwidget/astrowind/tree/main) [![Open in Gitpod](https://svgshare.com/i/xdi.svg)](https://gitpod.io/?on=gitpod#https://github.com/onwidget/astrowind) [![Open in StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/onwidget/astrowind)
|
||||||
|
|
||||||
|
> 🧑🚀 **Seasoned astronaut?** Delete this file `README.md`. Update `src/config.yaml` and contents. Have fun!
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
All commands are run from the root of the project, from a terminal:
|
||||||
|
|
||||||
|
| Command | Action |
|
||||||
|
| :-------------------- | :------------------------------------------------- |
|
||||||
|
| `npm install` | Installs dependencies |
|
||||||
|
| `npm run dev` | Starts local dev server at `localhost:3000` |
|
||||||
|
| `npm run build` | Build your production site to `./dist/` |
|
||||||
|
| `npm run preview` | Preview your build locally, before deploying |
|
||||||
|
| `npm run format` | Format codes with Prettier |
|
||||||
|
| `npm run lint:eslint` | Run Eslint |
|
||||||
|
| `npm run astro ...` | Run CLI commands like `astro add`, `astro preview` |
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
Basic configuration file: `./src/config.yaml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
site:
|
||||||
|
name: 'Example'
|
||||||
|
site: 'https://example.com'
|
||||||
|
base: '/' # Change this if you need to deploy to Github Pages, for example
|
||||||
|
trailingSlash: false # Generate permalinks with or without "/" at the end
|
||||||
|
|
||||||
|
googleSiteVerificationId: false # Or some value,
|
||||||
|
|
||||||
|
# Default SEO metadata
|
||||||
|
metadata:
|
||||||
|
title:
|
||||||
|
default: 'Example'
|
||||||
|
template: '%s — Example'
|
||||||
|
description: 'This is the default meta description of Example website'
|
||||||
|
robots:
|
||||||
|
index: true
|
||||||
|
follow: true
|
||||||
|
openGraph:
|
||||||
|
site_name: 'Example'
|
||||||
|
images:
|
||||||
|
- url: '~/assets/images/default.png'
|
||||||
|
width: 1200
|
||||||
|
height: 628
|
||||||
|
type: website
|
||||||
|
twitter:
|
||||||
|
handle: '@twitter_user'
|
||||||
|
site: '@twitter_user'
|
||||||
|
cardType: summary_large_image
|
||||||
|
|
||||||
|
i18n:
|
||||||
|
language: en
|
||||||
|
textDirection: ltr
|
||||||
|
|
||||||
|
apps:
|
||||||
|
blog:
|
||||||
|
isEnabled: true # If the blog will be enabled
|
||||||
|
postsPerPage: 6 # Number of posts per page
|
||||||
|
|
||||||
|
post:
|
||||||
|
isEnabled: true
|
||||||
|
permalink: '/blog/%slug%' # Variables: %slug%, %year%, %month%, %day%, %hour%, %minute%, %second%, %category%
|
||||||
|
robots:
|
||||||
|
index: true
|
||||||
|
|
||||||
|
list:
|
||||||
|
isEnabled: true
|
||||||
|
pathname: 'blog' # Blog main path, you can change this to "articles" (/articles)
|
||||||
|
robots:
|
||||||
|
index: true
|
||||||
|
|
||||||
|
category:
|
||||||
|
isEnabled: true
|
||||||
|
pathname: 'category' # Category main path /category/some-category, you can change this to "group" (/group/some-category)
|
||||||
|
robots:
|
||||||
|
index: true
|
||||||
|
|
||||||
|
tag:
|
||||||
|
isEnabled: true
|
||||||
|
pathname: 'tag' # Tag main path /tag/some-tag, you can change this to "topics" (/topics/some-category)
|
||||||
|
robots:
|
||||||
|
index: false
|
||||||
|
|
||||||
|
isRelatedPostsEnabled: true # If a widget with related posts is to be displayed below each post
|
||||||
|
relatedPostsCount: 4 # Number of related posts to display
|
||||||
|
|
||||||
|
analytics:
|
||||||
|
vendors:
|
||||||
|
googleAnalytics:
|
||||||
|
id: null # or "G-XXXXXXXXXX"
|
||||||
|
|
||||||
|
ui:
|
||||||
|
theme: 'system' # Values: "system" | "light" | "dark" | "light:only" | "dark:only"
|
||||||
|
```
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
#### Customize Design
|
||||||
|
|
||||||
|
To customize Font families, Colors or more Elements refer to the following files:
|
||||||
|
|
||||||
|
- `src/components/CustomStyles.astro`
|
||||||
|
- `src/assets/styles/tailwind.css`
|
||||||
|
|
||||||
|
### Deploy
|
||||||
|
|
||||||
|
#### Deploy to production (manual)
|
||||||
|
|
||||||
|
You can create an optimized production build with:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, your website is ready to be deployed. All generated files are located at
|
||||||
|
`dist` folder, which you can deploy the folder to any hosting service you
|
||||||
|
prefer.
|
||||||
|
|
||||||
|
#### Deploy to Netlify
|
||||||
|
|
||||||
|
Clone this repository on your own GitHub account and deploy it to Netlify:
|
||||||
|
|
||||||
|
[![Netlify Deploy button](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/onwidget/astrowind)
|
||||||
|
|
||||||
|
#### Deploy to Vercel
|
||||||
|
|
||||||
|
Clone this repository on your own GitHub account and deploy to Vercel:
|
||||||
|
|
||||||
|
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fonwidget%2Fastrowind)
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Frequently Asked Questions
|
||||||
|
|
||||||
|
- Why?
|
||||||
|
-
|
||||||
|
-
|
||||||
|
|
||||||
|
<br>
|
||||||
|
|
||||||
|
## Related projects
|
||||||
|
|
||||||
|
- [TailNext](https://tailnext.vercel.app/) - Free template using Next.js 14 and Tailwind CSS with the new App Router.
|
||||||
|
- [Qwind](https://qwind.pages.dev/) - Free template to make your website using Qwik + Tailwind CSS.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
If you have any ideas, suggestions or find any bugs, feel free to open a discussion, an issue or create a pull request.
|
||||||
|
That would be very useful for all of us and we would be happy to listen and take action.
|
||||||
|
|
||||||
|
## Acknowledgements
|
||||||
|
|
||||||
|
Initially created by [onWidget](https://onwidget.com) and maintained by a community of [contributors](https://github.com/onwidget/astrowind/graphs/contributors).
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
**AstroWind** is licensed under the MIT license — see the [LICENSE](./LICENSE.md) file for details.
|
90
v3/astro.config.ts
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
|
||||||
|
import sitemap from '@astrojs/sitemap';
|
||||||
|
import tailwind from '@astrojs/tailwind';
|
||||||
|
import mdx from '@astrojs/mdx';
|
||||||
|
import partytown from '@astrojs/partytown';
|
||||||
|
import icon from 'astro-icon';
|
||||||
|
import compress from 'astro-compress';
|
||||||
|
import type { AstroIntegration } from 'astro';
|
||||||
|
|
||||||
|
import astrowind from './vendor/integration';
|
||||||
|
|
||||||
|
import { readingTimeRemarkPlugin, responsiveTablesRehypePlugin, lazyImagesRehypePlugin } from './src/utils/frontmatter';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
const hasExternalScripts = false;
|
||||||
|
const whenExternalScripts = (items: (() => AstroIntegration) | (() => AstroIntegration)[] = []) =>
|
||||||
|
hasExternalScripts ? (Array.isArray(items) ? items.map((item) => item()) : [items()]) : [];
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
output: 'static',
|
||||||
|
|
||||||
|
integrations: [
|
||||||
|
tailwind({
|
||||||
|
applyBaseStyles: false,
|
||||||
|
}),
|
||||||
|
sitemap(),
|
||||||
|
mdx(),
|
||||||
|
icon({
|
||||||
|
include: {
|
||||||
|
tabler: ['*'],
|
||||||
|
'flat-color-icons': [
|
||||||
|
'template',
|
||||||
|
'gallery',
|
||||||
|
'approval',
|
||||||
|
'document',
|
||||||
|
'advertising',
|
||||||
|
'currency-exchange',
|
||||||
|
'voice-presentation',
|
||||||
|
'business-contact',
|
||||||
|
'database',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
...whenExternalScripts(() =>
|
||||||
|
partytown({
|
||||||
|
config: { forward: ['dataLayer.push'] },
|
||||||
|
})
|
||||||
|
),
|
||||||
|
|
||||||
|
compress({
|
||||||
|
CSS: true,
|
||||||
|
HTML: {
|
||||||
|
'html-minifier-terser': {
|
||||||
|
removeAttributeQuotes: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Image: false,
|
||||||
|
JavaScript: true,
|
||||||
|
SVG: false,
|
||||||
|
Logger: 1,
|
||||||
|
}),
|
||||||
|
|
||||||
|
astrowind({
|
||||||
|
config: './src/config.yaml',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
|
image: {
|
||||||
|
domains: ['cdn.pixabay.com'],
|
||||||
|
},
|
||||||
|
|
||||||
|
markdown: {
|
||||||
|
remarkPlugins: [readingTimeRemarkPlugin],
|
||||||
|
rehypePlugins: [responsiveTablesRehypePlugin, lazyImagesRehypePlugin],
|
||||||
|
},
|
||||||
|
|
||||||
|
vite: {
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'~': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
59
v3/eslint.config.js
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import astroEslintParser from 'astro-eslint-parser';
|
||||||
|
import eslintPluginAstro from 'eslint-plugin-astro';
|
||||||
|
import globals from 'globals';
|
||||||
|
import js from '@eslint/js';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
import typescriptParser from '@typescript-eslint/parser';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
js.configs.recommended,
|
||||||
|
...eslintPluginAstro.configs['flat/recommended'],
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
...globals.node,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.astro'],
|
||||||
|
languageOptions: {
|
||||||
|
parser: astroEslintParser,
|
||||||
|
parserOptions: {
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
extraFileExtensions: ['.astro'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ['**/*.{js,jsx,astro}'],
|
||||||
|
rules: {
|
||||||
|
'no-mixed-spaces-and-tabs': ['error', 'smart-tabs'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Define the configuration for `<script>` tag.
|
||||||
|
// Script in `<script>` is assigned a virtual file name with the `.js` extension.
|
||||||
|
files: ['**/*.{ts,tsx}', '**/*.astro/*.js'],
|
||||||
|
languageOptions: {
|
||||||
|
parser: typescriptParser,
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
// Note: you must disable the base rule as it can report incorrect errors
|
||||||
|
'no-unused-vars': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
argsIgnorePattern: '^_',
|
||||||
|
destructuredArrayIgnorePattern: '^_',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ignores: ['dist', 'node_modules', '.github', 'types.generated.d.ts', '.astro'],
|
||||||
|
},
|
||||||
|
];
|
9
v3/netlify.toml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[build]
|
||||||
|
publish = "dist"
|
||||||
|
command = "npm run build"
|
||||||
|
[build.processing.html]
|
||||||
|
pretty_urls = false
|
||||||
|
[[headers]]
|
||||||
|
for = "/_astro/*"
|
||||||
|
[headers.values]
|
||||||
|
Cache-Control = "public, max-age=31536000, immutable"
|
10865
v3/package-lock.json
generated
Normal file
69
v3/package.json
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
"name": "@onwidget/astrowind",
|
||||||
|
"version": "1.0.0-beta.46",
|
||||||
|
"description": "AstroWind: A free template using Astro 4.0 and Tailwind CSS. Astro starter theme.",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.1 || ^20.3.0 || >= 21.0.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"start": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"astro": "astro",
|
||||||
|
"check": "npm run check:astro && npm run check:eslint && npm run check:prettier",
|
||||||
|
"check:astro": "astro check",
|
||||||
|
"check:eslint": "eslint .",
|
||||||
|
"check:prettier": "prettier --check .",
|
||||||
|
"fix": "npm run fix:eslint && npm run fix:prettier",
|
||||||
|
"fix:eslint": "eslint --fix .",
|
||||||
|
"fix:prettier": "prettier -w ."
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/rss": "^4.0.7",
|
||||||
|
"@astrojs/sitemap": "^3.1.6",
|
||||||
|
"@astrolib/analytics": "^0.5.0",
|
||||||
|
"@astrolib/seo": "^1.0.0-beta.6",
|
||||||
|
"@fontsource-variable/inter": "^5.1.0",
|
||||||
|
"astro": "^4.15.5",
|
||||||
|
"astro-embed": "^0.7.2",
|
||||||
|
"astro-icon": "^1.1.1",
|
||||||
|
"limax": "4.1.0",
|
||||||
|
"lodash.merge": "^4.6.2",
|
||||||
|
"unpic": "^3.18.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@astrojs/check": "^0.9.3",
|
||||||
|
"@astrojs/mdx": "^3.1.6",
|
||||||
|
"@astrojs/partytown": "^2.1.2",
|
||||||
|
"@astrojs/tailwind": "5.1.0",
|
||||||
|
"@eslint/js": "^9.10.0",
|
||||||
|
"@iconify-json/flat-color-icons": "^1.2.0",
|
||||||
|
"@iconify-json/tabler": "^1.2.3",
|
||||||
|
"@tailwindcss/typography": "^0.5.15",
|
||||||
|
"@types/eslint__js": "^8.42.3",
|
||||||
|
"@types/js-yaml": "^4.0.9",
|
||||||
|
"@types/lodash.merge": "^4.6.9",
|
||||||
|
"@types/mdx": "^2.0.13",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.6.0",
|
||||||
|
"@typescript-eslint/parser": "^8.6.0",
|
||||||
|
"astro-compress": "2.3.1",
|
||||||
|
"astro-eslint-parser": "^1.0.3",
|
||||||
|
"eslint": "^9.10.0",
|
||||||
|
"eslint-plugin-astro": "^1.2.4",
|
||||||
|
"globals": "^15.9.0",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
|
"mdast-util-to-string": "^4.0.0",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
|
"prettier-plugin-astro": "^0.14.1",
|
||||||
|
"reading-time": "^1.5.0",
|
||||||
|
"sharp": "0.33.5",
|
||||||
|
"tailwind-merge": "^2.5.2",
|
||||||
|
"tailwindcss": "^3.4.11",
|
||||||
|
"typescript": "^5.6.2",
|
||||||
|
"typescript-eslint": "^8.6.0",
|
||||||
|
"unist-util-visit": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
11
v3/sandbox.config.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"infiniteLoopProtection": true,
|
||||||
|
"hardReloadOnChange": false,
|
||||||
|
"view": "browser",
|
||||||
|
"template": "node",
|
||||||
|
"container": {
|
||||||
|
"port": 3000,
|
||||||
|
"startScript": "start",
|
||||||
|
"node": "18"
|
||||||
|
}
|
||||||
|
}
|
BIN
v3/src/assets/favicons/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
BIN
v3/src/assets/favicons/favicon.ico
Normal file
After Width: | Height: | Size: 15 KiB |
9
v3/src/assets/favicons/favicon.svg
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||||
|
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||||
|
<style>
|
||||||
|
path { fill: #000; }
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
path { fill: #FFF; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 749 B |
BIN
v3/src/assets/images/app-store.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
v3/src/assets/images/default.png
Normal file
After Width: | Height: | Size: 563 KiB |
BIN
v3/src/assets/images/google-play.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
v3/src/assets/images/hero-image.png
Normal file
After Width: | Height: | Size: 539 KiB |
92
v3/src/assets/styles/tailwind.css
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.bg-page {
|
||||||
|
background-color: var(--aw-color-bg-page);
|
||||||
|
}
|
||||||
|
.bg-dark {
|
||||||
|
background-color: var(--aw-color-bg-page-dark);
|
||||||
|
}
|
||||||
|
.bg-light {
|
||||||
|
background-color: var(--aw-color-bg-page);
|
||||||
|
}
|
||||||
|
.text-page {
|
||||||
|
color: var(--aw-color-text-page);
|
||||||
|
}
|
||||||
|
.text-muted {
|
||||||
|
color: var(--aw-color-text-muted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.btn {
|
||||||
|
@apply inline-flex items-center justify-center rounded-full border-gray-400 border bg-transparent font-medium text-center text-base text-page leading-snug transition py-3.5 px-6 md:px-8 ease-in duration-200 focus:ring-blue-500 focus:ring-offset-blue-200 focus:ring-2 focus:ring-offset-2 hover:bg-gray-100 hover:border-gray-600 dark:text-slate-300 dark:border-slate-500 dark:hover:bg-slate-800 dark:hover:border-slate-800 cursor-pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
@apply btn font-semibold bg-primary text-white border-primary hover:bg-secondary hover:border-secondary hover:text-white dark:text-white dark:bg-primary dark:border-primary dark:hover:border-secondary dark:hover:bg-secondary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
@apply btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-tertiary {
|
||||||
|
@apply btn border-none shadow-none text-muted hover:text-gray-900 dark:text-gray-400 dark:hover:text-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#header.scroll > div:first-child {
|
||||||
|
@apply bg-page md:bg-white/90 md:backdrop-blur-md;
|
||||||
|
box-shadow: 0 0.375rem 1.5rem 0 rgb(140 152 164 / 13%);
|
||||||
|
}
|
||||||
|
.dark #header.scroll > div:first-child,
|
||||||
|
#header.scroll.dark > div:first-child {
|
||||||
|
@apply bg-page md:bg-[#030621e6] border-b border-gray-500/20;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
/* #header.scroll > div:last-child {
|
||||||
|
@apply py-3;
|
||||||
|
} */
|
||||||
|
|
||||||
|
#header.expanded nav {
|
||||||
|
position: fixed;
|
||||||
|
top: 70px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 70px !important;
|
||||||
|
padding: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown:focus .dropdown-menu,
|
||||||
|
.dropdown:focus-within .dropdown-menu,
|
||||||
|
.dropdown:hover .dropdown-menu {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
[astro-icon].icon-light > * {
|
||||||
|
stroke-width: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
[astro-icon].icon-bold > * {
|
||||||
|
stroke-width: 2.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-aw-toggle-menu] path {
|
||||||
|
@apply transition;
|
||||||
|
}
|
||||||
|
[data-aw-toggle-menu].expanded g > path:first-child {
|
||||||
|
@apply -rotate-45 translate-y-[15px] translate-x-[-3px];
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-aw-toggle-menu].expanded g > path:last-child {
|
||||||
|
@apply rotate-45 translate-y-[-8px] translate-x-[14px];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* To deprecated */
|
||||||
|
|
||||||
|
.dd *:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
63
v3/src/components/CustomStyles.astro
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
import '@fontsource-variable/inter';
|
||||||
|
|
||||||
|
// 'DM Sans'
|
||||||
|
// Nunito
|
||||||
|
// Dosis
|
||||||
|
// Outfit
|
||||||
|
// Roboto
|
||||||
|
// Literata
|
||||||
|
// 'IBM Plex Sans'
|
||||||
|
// Karla
|
||||||
|
// Poppins
|
||||||
|
// 'Fira Sans'
|
||||||
|
// 'Libre Franklin'
|
||||||
|
// Inconsolata
|
||||||
|
// Raleway
|
||||||
|
// Oswald
|
||||||
|
// 'Space Grotesk'
|
||||||
|
// Urbanist
|
||||||
|
---
|
||||||
|
|
||||||
|
<style is:inline>
|
||||||
|
:root {
|
||||||
|
--aw-font-sans: 'Inter Variable';
|
||||||
|
--aw-font-serif: 'Inter Variable';
|
||||||
|
--aw-font-heading: 'Inter Variable';
|
||||||
|
|
||||||
|
--aw-color-primary: rgb(1 97 239);
|
||||||
|
--aw-color-secondary: rgb(1 84 207);
|
||||||
|
--aw-color-accent: rgb(109 40 217);
|
||||||
|
|
||||||
|
--aw-color-text-heading: rgb(0 0 0);
|
||||||
|
--aw-color-text-default: rgb(16 16 16);
|
||||||
|
--aw-color-text-muted: rgb(16 16 16 / 66%);
|
||||||
|
--aw-color-bg-page: rgb(255 255 255);
|
||||||
|
|
||||||
|
--aw-color-bg-page-dark: rgb(3 6 32);
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background-color: lavender;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--aw-font-sans: 'Inter Variable';
|
||||||
|
--aw-font-serif: 'Inter Variable';
|
||||||
|
--aw-font-heading: 'Inter Variable';
|
||||||
|
|
||||||
|
--aw-color-primary: rgb(1 97 239);
|
||||||
|
--aw-color-secondary: rgb(1 84 207);
|
||||||
|
--aw-color-accent: rgb(109 40 217);
|
||||||
|
|
||||||
|
--aw-color-text-heading: rgb(247, 248, 248);
|
||||||
|
--aw-color-text-default: rgb(229 236 246);
|
||||||
|
--aw-color-text-muted: rgb(229 236 246 / 66%);
|
||||||
|
--aw-color-bg-page: rgb(3 6 32);
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background-color: black;
|
||||||
|
color: snow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
10
v3/src/components/Favicons.astro
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
import favIcon from '~/assets/favicons/favicon.ico';
|
||||||
|
import favIconSvg from '~/assets/favicons/favicon.svg';
|
||||||
|
import appleTouchIcon from '~/assets/favicons/apple-touch-icon.png';
|
||||||
|
---
|
||||||
|
|
||||||
|
<link rel="shortcut icon" href={favIcon} />
|
||||||
|
<link rel="icon" type="image/svg+xml" href={favIconSvg.src} />
|
||||||
|
<link rel="mask-icon" href={favIconSvg.src} color="#8D46E7" />
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href={appleTouchIcon.src} />
|
9
v3/src/components/Logo.astro
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
import { SITE } from 'astrowind:config';
|
||||||
|
---
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="self-center ml-2 rtl:ml-0 rtl:mr-2 text-2xl md:text-xl font-bold text-gray-900 whitespace-nowrap dark:text-white"
|
||||||
|
>
|
||||||
|
🚀 {SITE?.name}
|
||||||
|
</span>
|
14
v3/src/components/blog/Grid.astro
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
import Item from '~/components/blog/GridItem.astro';
|
||||||
|
import type { Post } from '~/types';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
posts: Array<Post>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { posts } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="grid gap-6 row-gap-5 md:grid-cols-2 lg:grid-cols-4 -mb-6">
|
||||||
|
{posts.map((post) => <Item post={post} />)}
|
||||||
|
</div>
|
69
v3/src/components/blog/GridItem.astro
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
import { APP_BLOG } from 'astrowind:config';
|
||||||
|
import type { Post } from '~/types';
|
||||||
|
|
||||||
|
import Image from '~/components/common/Image.astro';
|
||||||
|
|
||||||
|
import { findImage } from '~/utils/images';
|
||||||
|
import { getPermalink } from '~/utils/permalinks';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
post: Post;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { post } = Astro.props;
|
||||||
|
const image = await findImage(post.image);
|
||||||
|
|
||||||
|
const link = APP_BLOG?.post?.isEnabled ? getPermalink(post.permalink, 'post') : '';
|
||||||
|
---
|
||||||
|
|
||||||
|
<article class="mb-6 transition">
|
||||||
|
<div class="relative md:h-64 bg-gray-400 dark:bg-slate-700 rounded shadow-lg mb-6">
|
||||||
|
{
|
||||||
|
image &&
|
||||||
|
(link ? (
|
||||||
|
<a href={link}>
|
||||||
|
<Image
|
||||||
|
src={image}
|
||||||
|
class="w-full md:h-full rounded shadow-lg bg-gray-400 dark:bg-slate-700"
|
||||||
|
widths={[400, 900]}
|
||||||
|
width={400}
|
||||||
|
sizes="(max-width: 900px) 400px, 900px"
|
||||||
|
alt={post.title}
|
||||||
|
aspectRatio="16:9"
|
||||||
|
layout="cover"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
src={image}
|
||||||
|
class="w-full md:h-full rounded shadow-lg bg-gray-400 dark:bg-slate-700"
|
||||||
|
widths={[400, 900]}
|
||||||
|
width={400}
|
||||||
|
sizes="(max-width: 900px) 400px, 900px"
|
||||||
|
alt={post.title}
|
||||||
|
aspectRatio="16:9"
|
||||||
|
layout="cover"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="text-xl sm:text-2xl font-bold leading-tight mb-2 font-heading dark:text-slate-300">
|
||||||
|
{
|
||||||
|
link ? (
|
||||||
|
<a class="inline-block hover:text-primary dark:hover:text-blue-700 transition ease-in duration-200" href={link}>
|
||||||
|
{post.title}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
post.title
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p class="text-muted dark:text-slate-400 text-lg">{post.excerpt}</p>
|
||||||
|
</article>
|
12
v3/src/components/blog/Headline.astro
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
const { title = await Astro.slots.render('default'), subtitle = await Astro.slots.render('subtitle') } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<header class="mb-8 md:mb-16 text-center max-w-3xl mx-auto">
|
||||||
|
<h1 class="text-4xl md:text-5xl font-bold leading-tighter tracking-tighter font-heading" set:html={title} />
|
||||||
|
{
|
||||||
|
subtitle && (
|
||||||
|
<div class="mt-2 md:mt-3 mx-auto text-xl text-gray-500 dark:text-slate-400 font-medium" set:html={subtitle} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</header>
|
20
v3/src/components/blog/List.astro
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
import Item from '~/components/blog/ListItem.astro';
|
||||||
|
import type { Post } from '~/types';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
posts: Array<Post>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { posts } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
{
|
||||||
|
posts.map((post) => (
|
||||||
|
<li class="mb-12 md:mb-20">
|
||||||
|
<Item post={post} />
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
118
v3/src/components/blog/ListItem.astro
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
---
|
||||||
|
import type { ImageMetadata } from 'astro';
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import Image from '~/components/common/Image.astro';
|
||||||
|
import PostTags from '~/components/blog/Tags.astro';
|
||||||
|
|
||||||
|
import { APP_BLOG } from 'astrowind:config';
|
||||||
|
import type { Post } from '~/types';
|
||||||
|
|
||||||
|
import { getPermalink } from '~/utils/permalinks';
|
||||||
|
import { findImage } from '~/utils/images';
|
||||||
|
import { getFormattedDate } from '~/utils/utils';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
post: Post;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { post } = Astro.props;
|
||||||
|
const image = (await findImage(post.image)) as ImageMetadata | undefined;
|
||||||
|
|
||||||
|
const link = APP_BLOG?.post?.isEnabled ? getPermalink(post.permalink, 'post') : '';
|
||||||
|
---
|
||||||
|
|
||||||
|
<article class={`max-w-md mx-auto md:max-w-none grid gap-6 md:gap-8 ${image ? 'md:grid-cols-2' : ''}`}>
|
||||||
|
{
|
||||||
|
image &&
|
||||||
|
(link ? (
|
||||||
|
<a class="relative block group" href={link ?? 'javascript:void(0)'}>
|
||||||
|
<div class="relative h-0 pb-[56.25%] md:pb-[75%] md:h-72 lg:pb-[56.25%] overflow-hidden bg-gray-400 dark:bg-slate-700 rounded shadow-lg">
|
||||||
|
{image && (
|
||||||
|
<Image
|
||||||
|
src={image}
|
||||||
|
class="absolute inset-0 object-cover w-full h-full mb-6 rounded shadow-lg bg-gray-400 dark:bg-slate-700"
|
||||||
|
widths={[400, 900]}
|
||||||
|
width={900}
|
||||||
|
sizes="(max-width: 900px) 400px, 900px"
|
||||||
|
alt={post.title}
|
||||||
|
aspectRatio="16:9"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<div class="relative h-0 pb-[56.25%] md:pb-[75%] md:h-72 lg:pb-[56.25%] overflow-hidden bg-gray-400 dark:bg-slate-700 rounded shadow-lg">
|
||||||
|
{image && (
|
||||||
|
<Image
|
||||||
|
src={image}
|
||||||
|
class="absolute inset-0 object-cover w-full h-full mb-6 rounded shadow-lg bg-gray-400 dark:bg-slate-700"
|
||||||
|
widths={[400, 900]}
|
||||||
|
width={900}
|
||||||
|
sizes="(max-width: 900px) 400px, 900px"
|
||||||
|
alt={post.title}
|
||||||
|
aspectRatio="16:9"
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
<div class="mt-2">
|
||||||
|
<header>
|
||||||
|
<div class="mb-1">
|
||||||
|
<span class="text-sm">
|
||||||
|
<Icon name="tabler:clock" class="w-3.5 h-3.5 inline-block -mt-0.5 dark:text-gray-400" />
|
||||||
|
<time datetime={String(post.publishDate)} class="inline-block">{getFormattedDate(post.publishDate)}</time>
|
||||||
|
{
|
||||||
|
post.author && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
· <Icon name="tabler:user" class="w-3.5 h-3.5 inline-block -mt-0.5 dark:text-gray-400" />
|
||||||
|
<span>{post.author.replaceAll('-', ' ')}</span>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
post.category && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
·{' '}
|
||||||
|
<a class="hover:underline" href={getPermalink(post.category.slug, 'category')}>
|
||||||
|
{post.category.title}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-xl sm:text-2xl font-bold leading-tight mb-2 font-heading dark:text-slate-300">
|
||||||
|
{
|
||||||
|
link ? (
|
||||||
|
<a
|
||||||
|
class="inline-block hover:text-primary dark:hover:text-blue-700 transition ease-in duration-200"
|
||||||
|
href={link}
|
||||||
|
>
|
||||||
|
{post.title}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
post.title
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</h2>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{post.excerpt && <p class="flex-grow text-muted dark:text-slate-400 text-lg">{post.excerpt}</p>}
|
||||||
|
{
|
||||||
|
post.tags && Array.isArray(post.tags) ? (
|
||||||
|
<footer class="mt-5">
|
||||||
|
<PostTags tags={post.tags} />
|
||||||
|
</footer>
|
||||||
|
) : (
|
||||||
|
<Fragment />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</article>
|
36
v3/src/components/blog/Pagination.astro
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import { getPermalink } from '~/utils/permalinks';
|
||||||
|
import Button from '~/components/ui/Button.astro';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
prevUrl?: string;
|
||||||
|
nextUrl?: string;
|
||||||
|
prevText?: string;
|
||||||
|
nextText?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { prevUrl, nextUrl, prevText = 'Newer posts', nextText = 'Older posts' } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
(prevUrl || nextUrl) && (
|
||||||
|
<div class="container flex">
|
||||||
|
<div class="flex flex-row mx-auto container justify-between">
|
||||||
|
<Button
|
||||||
|
variant="tertiary"
|
||||||
|
class={`md:px-3 px-3 mr-2 ${!prevUrl ? 'invisible' : ''}`}
|
||||||
|
href={getPermalink(prevUrl)}
|
||||||
|
>
|
||||||
|
<Icon name="tabler:chevron-left" class="w-6 h-6" />
|
||||||
|
<p class="ml-2">{prevText}</p>
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button variant="tertiary" class={`md:px-3 px-3 ${!nextUrl ? 'invisible' : ''}`} href={getPermalink(nextUrl)}>
|
||||||
|
<span class="mr-2">{nextText}</span>
|
||||||
|
<Icon name="tabler:chevron-right" class="w-6 h-6" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
28
v3/src/components/blog/RelatedPosts.astro
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
import { APP_BLOG } from 'astrowind:config';
|
||||||
|
|
||||||
|
import { getRelatedPosts } from '~/utils/blog';
|
||||||
|
import BlogHighlightedPosts from '../widgets/BlogHighlightedPosts.astro';
|
||||||
|
import type { Post } from '~/types';
|
||||||
|
import { getBlogPermalink } from '~/utils/permalinks';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
post: Post;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { post } = Astro.props;
|
||||||
|
|
||||||
|
const relatedPosts = post.tags ? await getRelatedPosts(post, 4) : [];
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
APP_BLOG.isRelatedPostsEnabled ? (
|
||||||
|
<BlogHighlightedPosts
|
||||||
|
classes={{ container: 'pt-0 lg:pt-0 md:pt-0' }}
|
||||||
|
title="Related Posts"
|
||||||
|
linkText="View All Posts"
|
||||||
|
linkUrl={getBlogPermalink()}
|
||||||
|
postIds={relatedPosts.map((post) => post.id)}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
99
v3/src/components/blog/SinglePost.astro
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
|
||||||
|
import Image from '~/components/common/Image.astro';
|
||||||
|
import PostTags from '~/components/blog/Tags.astro';
|
||||||
|
import SocialShare from '~/components/common/SocialShare.astro';
|
||||||
|
|
||||||
|
import { getPermalink } from '~/utils/permalinks';
|
||||||
|
import { getFormattedDate } from '~/utils/utils';
|
||||||
|
|
||||||
|
import type { Post } from '~/types';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
post: Post;
|
||||||
|
url: string | URL;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { post, url } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="py-8 sm:py-16 lg:py-20 mx-auto">
|
||||||
|
<article>
|
||||||
|
<header class={post.image ? '' : ''}>
|
||||||
|
<div class="flex justify-between flex-col sm:flex-row max-w-3xl mx-auto mt-0 mb-2 px-4 sm:px-6 sm:items-center">
|
||||||
|
<p>
|
||||||
|
<Icon name="tabler:clock" class="w-4 h-4 inline-block -mt-0.5 dark:text-gray-400" />
|
||||||
|
<time datetime={String(post.publishDate)} class="inline-block">{getFormattedDate(post.publishDate)}</time>
|
||||||
|
{
|
||||||
|
post.author && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
· <Icon name="tabler:user" class="w-4 h-4 inline-block -mt-0.5 dark:text-gray-400" />
|
||||||
|
<span class="inline-block">{post.author}</span>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
post.category && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
·{' '}
|
||||||
|
<a class="hover:underline inline-block" href={getPermalink(post.category.slug, 'category')}>
|
||||||
|
{post.category.title}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
post.readingTime && (
|
||||||
|
<>
|
||||||
|
· <span>{post.readingTime}</span> min read
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1
|
||||||
|
class="px-4 sm:px-6 max-w-3xl mx-auto text-4xl md:text-5xl font-bold leading-tighter tracking-tighter font-heading"
|
||||||
|
>
|
||||||
|
{post.title}
|
||||||
|
</h1>
|
||||||
|
<p
|
||||||
|
class="max-w-3xl mx-auto mt-4 mb-8 px-4 sm:px-6 text-xl md:text-2xl text-muted dark:text-slate-400 text-justify"
|
||||||
|
>
|
||||||
|
{post.excerpt}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{
|
||||||
|
post.image ? (
|
||||||
|
<Image
|
||||||
|
src={post.image}
|
||||||
|
class="max-w-full lg:max-w-[900px] mx-auto mb-6 sm:rounded-md bg-gray-400 dark:bg-slate-700"
|
||||||
|
widths={[400, 900]}
|
||||||
|
sizes="(max-width: 900px) 400px, 900px"
|
||||||
|
alt={post?.excerpt || ''}
|
||||||
|
width={900}
|
||||||
|
height={506}
|
||||||
|
loading="eager"
|
||||||
|
decoding="async"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div class="max-w-3xl mx-auto px-4 sm:px-6">
|
||||||
|
<div class="border-t dark:border-slate-700" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</header>
|
||||||
|
<div
|
||||||
|
class="mx-auto px-6 sm:px-6 max-w-3xl prose prose-md lg:prose-xl dark:prose-invert dark:prose-headings:text-slate-300 prose-headings:font-heading prose-headings:leading-tighter prose-headings:tracking-tighter prose-headings:font-bold prose-a:text-primary dark:prose-a:text-blue-400 prose-img:rounded-md prose-img:shadow-lg mt-8 prose-headings:scroll-mt-[80px] prose-li:my-0"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
<div class="mx-auto px-6 sm:px-6 max-w-3xl mt-8 flex justify-between flex-col sm:flex-row">
|
||||||
|
<PostTags tags={post.tags} class="mr-5 rtl:mr-0 rtl:ml-5" />
|
||||||
|
<SocialShare url={url} text={post.title} class="mt-5 sm:mt-1 align-middle text-gray-500 dark:text-slate-600" />
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</section>
|
45
v3/src/components/blog/Tags.astro
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
import { getPermalink } from '~/utils/permalinks';
|
||||||
|
|
||||||
|
import { APP_BLOG } from 'astrowind:config';
|
||||||
|
import type { Post } from '~/types';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
tags: Post['tags'];
|
||||||
|
class?: string;
|
||||||
|
title?: string | undefined;
|
||||||
|
isCategory?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { tags, class: className = 'text-sm', title = undefined, isCategory = false } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
tags && Array.isArray(tags) && (
|
||||||
|
<>
|
||||||
|
<>
|
||||||
|
{title !== undefined && (
|
||||||
|
<span class="align-super font-normal underline underline-offset-4 decoration-2 dark:text-slate-400">
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
<ul class={className}>
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<li class="bg-gray-100 dark:bg-slate-700 inline-block mr-2 rtl:mr-0 rtl:ml-2 mb-2 py-0.5 px-2 lowercase font-medium">
|
||||||
|
{!APP_BLOG?.tag?.isEnabled ? (
|
||||||
|
tag.title
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
href={getPermalink(tag.slug, isCategory ? 'category' : 'tag')}
|
||||||
|
class="text-muted dark:text-slate-300 hover:text-primary dark:hover:text-gray-200"
|
||||||
|
>
|
||||||
|
{tag.title}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
20
v3/src/components/blog/ToBlogLink.astro
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import { getBlogPermalink } from '~/utils/permalinks';
|
||||||
|
import { I18N } from 'astrowind:config';
|
||||||
|
import Button from '~/components/ui/Button.astro';
|
||||||
|
|
||||||
|
const { textDirection } = I18N;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="mx-auto px-6 sm:px-6 max-w-3xl pt-8 md:pt-4 pb-12 md:pb-20">
|
||||||
|
<Button variant="tertiary" class="px-3 md:px-3" href={getBlogPermalink()}>
|
||||||
|
{
|
||||||
|
textDirection === 'rtl' ? (
|
||||||
|
<Icon name="tabler:chevron-right" class="w-5 h-5 mr-1 -ml-1.5 rtl:-mr-1.5 rtl:ml-1" />
|
||||||
|
) : (
|
||||||
|
<Icon name="tabler:chevron-left" class="w-5 h-5 mr-1 -ml-1.5 rtl:-mr-1.5 rtl:ml-1" />
|
||||||
|
)
|
||||||
|
} Back to Blog
|
||||||
|
</Button>
|
||||||
|
</div>
|
13
v3/src/components/common/Analytics.astro
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
import { GoogleAnalytics } from '@astrolib/analytics';
|
||||||
|
import { ANALYTICS } from 'astrowind:config';
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
ANALYTICS?.vendors?.googleAnalytics?.id ? (
|
||||||
|
<GoogleAnalytics
|
||||||
|
id={String(ANALYTICS.vendors.googleAnalytics.id)}
|
||||||
|
partytown={ANALYTICS?.vendors?.googleAnalytics?.partytown}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
33
v3/src/components/common/ApplyColorMode.astro
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
import { UI } from 'astrowind:config';
|
||||||
|
|
||||||
|
// TODO: This code is temporary
|
||||||
|
---
|
||||||
|
|
||||||
|
<script is:inline define:vars={{ defaultTheme: UI.theme || 'system' }}>
|
||||||
|
function applyTheme(theme) {
|
||||||
|
if (theme === 'dark') {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
const matches = document.querySelectorAll('[data-aw-toggle-color-scheme] > input');
|
||||||
|
|
||||||
|
if (matches && matches.length) {
|
||||||
|
matches.forEach((elem) => {
|
||||||
|
elem.checked = theme !== 'dark';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((defaultTheme && defaultTheme.endsWith(':only')) || (!localStorage.theme && defaultTheme !== 'system')) {
|
||||||
|
applyTheme(defaultTheme.replace(':only', ''));
|
||||||
|
} else if (
|
||||||
|
localStorage.theme === 'dark' ||
|
||||||
|
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||||
|
) {
|
||||||
|
applyTheme('dark');
|
||||||
|
} else {
|
||||||
|
applyTheme('light');
|
||||||
|
}
|
||||||
|
</script>
|
162
v3/src/components/common/BasicScripts.astro
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
---
|
||||||
|
import { UI } from 'astrowind:config';
|
||||||
|
---
|
||||||
|
|
||||||
|
<script is:inline define:vars={{ defaultTheme: UI.theme }}>
|
||||||
|
if (window.basic_script) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.basic_script = true;
|
||||||
|
|
||||||
|
function applyTheme(theme) {
|
||||||
|
if (theme === 'dark') {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const initTheme = function () {
|
||||||
|
if ((defaultTheme && defaultTheme.endsWith(':only')) || (!localStorage.theme && defaultTheme !== 'system')) {
|
||||||
|
applyTheme(defaultTheme.replace(':only', ''));
|
||||||
|
} else if (
|
||||||
|
localStorage.theme === 'dark' ||
|
||||||
|
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||||
|
) {
|
||||||
|
applyTheme('dark');
|
||||||
|
} else {
|
||||||
|
applyTheme('light');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
initTheme();
|
||||||
|
|
||||||
|
function attachEvent(selector, event, fn) {
|
||||||
|
const matches = typeof selector === 'string' ? document.querySelectorAll(selector) : selector;
|
||||||
|
if (matches && matches.length) {
|
||||||
|
matches.forEach((elem) => {
|
||||||
|
elem.addEventListener(event, (e) => fn(e, elem), false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onLoad = function () {
|
||||||
|
let lastKnownScrollPosition = window.scrollY;
|
||||||
|
let ticking = true;
|
||||||
|
|
||||||
|
attachEvent('#header nav', 'click', function () {
|
||||||
|
document.querySelector('[data-aw-toggle-menu]')?.classList.remove('expanded');
|
||||||
|
document.body.classList.remove('overflow-hidden');
|
||||||
|
document.getElementById('header')?.classList.remove('h-screen');
|
||||||
|
document.getElementById('header')?.classList.remove('expanded');
|
||||||
|
document.getElementById('header')?.classList.remove('bg-page');
|
||||||
|
document.querySelector('#header nav')?.classList.add('hidden');
|
||||||
|
document.querySelector('#header > div > div:last-child')?.classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
attachEvent('[data-aw-toggle-menu]', 'click', function (_, elem) {
|
||||||
|
elem.classList.toggle('expanded');
|
||||||
|
document.body.classList.toggle('overflow-hidden');
|
||||||
|
document.getElementById('header')?.classList.toggle('h-screen');
|
||||||
|
document.getElementById('header')?.classList.toggle('expanded');
|
||||||
|
document.getElementById('header')?.classList.toggle('bg-page');
|
||||||
|
document.querySelector('#header nav')?.classList.toggle('hidden');
|
||||||
|
document.querySelector('#header > div > div:last-child')?.classList.toggle('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
attachEvent('[data-aw-toggle-color-scheme]', 'click', function () {
|
||||||
|
if (defaultTheme.endsWith(':only')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.documentElement.classList.toggle('dark');
|
||||||
|
localStorage.theme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
|
||||||
|
});
|
||||||
|
|
||||||
|
attachEvent('[data-aw-social-share]', 'click', function (_, elem) {
|
||||||
|
const network = elem.getAttribute('data-aw-social-share');
|
||||||
|
const url = encodeURIComponent(elem.getAttribute('data-aw-url'));
|
||||||
|
const text = encodeURIComponent(elem.getAttribute('data-aw-text'));
|
||||||
|
|
||||||
|
let href;
|
||||||
|
switch (network) {
|
||||||
|
case 'facebook':
|
||||||
|
href = `https://www.facebook.com/sharer.php?u=${url}`;
|
||||||
|
break;
|
||||||
|
case 'twitter':
|
||||||
|
href = `https://twitter.com/intent/tweet?url=${url}&text=${text}`;
|
||||||
|
break;
|
||||||
|
case 'linkedin':
|
||||||
|
href = `https://www.linkedin.com/shareArticle?mini=true&url=${url}&title=${text}`;
|
||||||
|
break;
|
||||||
|
case 'whatsapp':
|
||||||
|
href = `https://wa.me/?text=${text}%20${url}`;
|
||||||
|
break;
|
||||||
|
case 'mail':
|
||||||
|
href = `mailto:?subject=%22${text}%22&body=${text}%20${url}`;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newlink = document.createElement('a');
|
||||||
|
newlink.target = '_blank';
|
||||||
|
newlink.href = href;
|
||||||
|
newlink.click();
|
||||||
|
});
|
||||||
|
|
||||||
|
const screenSize = window.matchMedia('(max-width: 767px)');
|
||||||
|
screenSize.addEventListener('change', function () {
|
||||||
|
document.querySelector('[data-aw-toggle-menu]')?.classList.remove('expanded');
|
||||||
|
document.body.classList.remove('overflow-hidden');
|
||||||
|
document.getElementById('header')?.classList.remove('h-screen');
|
||||||
|
document.getElementById('header')?.classList.remove('expanded');
|
||||||
|
document.getElementById('header')?.classList.remove('bg-page');
|
||||||
|
document.querySelector('#header nav')?.classList.add('hidden');
|
||||||
|
document.querySelector('#header > div > div:last-child')?.classList.add('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
function applyHeaderStylesOnScroll() {
|
||||||
|
const header = document.querySelector('#header[data-aw-sticky-header]');
|
||||||
|
if (!header) return;
|
||||||
|
if (lastKnownScrollPosition > 60 && !header.classList.contains('scroll')) {
|
||||||
|
header.classList.add('scroll');
|
||||||
|
} else if (lastKnownScrollPosition <= 60 && header.classList.contains('scroll')) {
|
||||||
|
header.classList.remove('scroll');
|
||||||
|
}
|
||||||
|
ticking = false;
|
||||||
|
}
|
||||||
|
applyHeaderStylesOnScroll();
|
||||||
|
|
||||||
|
attachEvent([document], 'scroll', function () {
|
||||||
|
lastKnownScrollPosition = window.scrollY;
|
||||||
|
|
||||||
|
if (!ticking) {
|
||||||
|
window.requestAnimationFrame(() => {
|
||||||
|
applyHeaderStylesOnScroll();
|
||||||
|
});
|
||||||
|
ticking = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const onPageShow = function () {
|
||||||
|
document.documentElement.classList.add('motion-safe:scroll-smooth');
|
||||||
|
const elem = document.querySelector('[data-aw-toggle-menu]');
|
||||||
|
if (elem) {
|
||||||
|
elem.classList.remove('expanded');
|
||||||
|
}
|
||||||
|
document.body.classList.remove('overflow-hidden');
|
||||||
|
document.getElementById('header')?.classList.remove('h-screen');
|
||||||
|
document.getElementById('header')?.classList.remove('expanded');
|
||||||
|
document.querySelector('#header nav')?.classList.add('hidden');
|
||||||
|
};
|
||||||
|
|
||||||
|
window.onload = onLoad;
|
||||||
|
window.onpageshow = onPageShow;
|
||||||
|
|
||||||
|
document.addEventListener('astro:after-swap', () => {
|
||||||
|
initTheme();
|
||||||
|
onLoad();
|
||||||
|
onPageShow();
|
||||||
|
});
|
||||||
|
</script>
|
8
v3/src/components/common/CommonMeta.astro
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
import { getAsset } from '~/utils/permalinks';
|
||||||
|
---
|
||||||
|
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
|
||||||
|
<link rel="sitemap" href={getAsset('/sitemap-index.xml')} />
|
64
v3/src/components/common/Image.astro
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
import type { HTMLAttributes } from 'astro/types';
|
||||||
|
import { findImage } from '~/utils/images';
|
||||||
|
import {
|
||||||
|
getImagesOptimized,
|
||||||
|
astroAsseetsOptimizer,
|
||||||
|
unpicOptimizer,
|
||||||
|
isUnpicCompatible,
|
||||||
|
type ImageProps,
|
||||||
|
} from '~/utils/images-optimization';
|
||||||
|
|
||||||
|
type Props = ImageProps;
|
||||||
|
type ImageType = {
|
||||||
|
src: string;
|
||||||
|
attributes: HTMLAttributes<'img'>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = Astro.props;
|
||||||
|
|
||||||
|
if (props.alt === undefined || props.alt === null) {
|
||||||
|
throw new Error();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof props.width === 'string') {
|
||||||
|
props.width = parseInt(props.width);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof props.height === 'string') {
|
||||||
|
props.height = parseInt(props.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.loading) {
|
||||||
|
props.loading = 'lazy';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.decoding) {
|
||||||
|
props.decoding = 'async';
|
||||||
|
}
|
||||||
|
|
||||||
|
const _image = await findImage(props.src);
|
||||||
|
|
||||||
|
let image: ImageType | undefined = undefined;
|
||||||
|
|
||||||
|
if (typeof _image === 'string') {
|
||||||
|
if ((_image.startsWith('http://') || _image.startsWith('https://')) && isUnpicCompatible(_image)) {
|
||||||
|
image = await getImagesOptimized(_image, props, unpicOptimizer);
|
||||||
|
} else {
|
||||||
|
image = {
|
||||||
|
src: _image,
|
||||||
|
attributes: { ...props, src: undefined },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (_image) {
|
||||||
|
image = await getImagesOptimized(_image, props, astroAsseetsOptimizer);
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
!image ? (
|
||||||
|
<Fragment />
|
||||||
|
) : (
|
||||||
|
<img src={image.src} crossorigin="anonymous" referrerpolicy="no-referrer" {...image.attributes} />
|
||||||
|
)
|
||||||
|
}
|
68
v3/src/components/common/Metadata.astro
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
---
|
||||||
|
import merge from 'lodash.merge';
|
||||||
|
import { AstroSeo } from '@astrolib/seo';
|
||||||
|
|
||||||
|
import type { Props as AstroSeoProps } from '@astrolib/seo';
|
||||||
|
|
||||||
|
import { SITE, METADATA, I18N } from 'astrowind:config';
|
||||||
|
import type { MetaData } from '~/types';
|
||||||
|
import { getCanonical } from '~/utils/permalinks';
|
||||||
|
|
||||||
|
import { adaptOpenGraphImages } from '~/utils/images';
|
||||||
|
|
||||||
|
export interface Props extends MetaData {
|
||||||
|
dontUseTitleTemplate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
ignoreTitleTemplate = false,
|
||||||
|
canonical = String(getCanonical(String(Astro.url.pathname))),
|
||||||
|
robots = {},
|
||||||
|
description,
|
||||||
|
openGraph = {},
|
||||||
|
twitter = {},
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const seoProps: AstroSeoProps = merge(
|
||||||
|
{
|
||||||
|
title: '',
|
||||||
|
titleTemplate: '%s',
|
||||||
|
canonical: canonical,
|
||||||
|
noindex: true,
|
||||||
|
nofollow: true,
|
||||||
|
description: undefined,
|
||||||
|
openGraph: {
|
||||||
|
url: canonical,
|
||||||
|
site_name: SITE?.name,
|
||||||
|
images: [],
|
||||||
|
locale: I18N?.language || 'en',
|
||||||
|
type: 'website',
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
cardType: openGraph?.images?.length ? 'summary_large_image' : 'summary',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: METADATA?.title?.default,
|
||||||
|
titleTemplate: METADATA?.title?.template,
|
||||||
|
noindex: typeof METADATA?.robots?.index !== 'undefined' ? !METADATA.robots.index : undefined,
|
||||||
|
nofollow: typeof METADATA?.robots?.follow !== 'undefined' ? !METADATA.robots.follow : undefined,
|
||||||
|
description: METADATA?.description,
|
||||||
|
openGraph: METADATA?.openGraph,
|
||||||
|
twitter: METADATA?.twitter,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: title,
|
||||||
|
titleTemplate: ignoreTitleTemplate ? '%s' : undefined,
|
||||||
|
canonical: canonical,
|
||||||
|
noindex: typeof robots?.index !== 'undefined' ? !robots.index : undefined,
|
||||||
|
nofollow: typeof robots?.follow !== 'undefined' ? !robots.follow : undefined,
|
||||||
|
description: description,
|
||||||
|
openGraph: { url: canonical, ...openGraph },
|
||||||
|
twitter: twitter,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
---
|
||||||
|
|
||||||
|
<AstroSeo {...{ ...seoProps, openGraph: await adaptOpenGraphImages(seoProps?.openGraph, Astro.site) }} />
|
5
v3/src/components/common/SiteVerification.astro
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
import { SITE } from 'astrowind:config';
|
||||||
|
---
|
||||||
|
|
||||||
|
{SITE.googleSiteVerificationId && <meta name="google-site-verification" content={SITE.googleSiteVerificationId} />}
|
65
v3/src/components/common/SocialShare.astro
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
text: string;
|
||||||
|
url: string | URL;
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { text, url, class: className = 'inline-block' } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class={className}>
|
||||||
|
<span class="align-super font-bold text-slate-500 dark:text-slate-400">Share:</span>
|
||||||
|
<button
|
||||||
|
class="ml-2 rtl:ml-0 rtl:mr-2"
|
||||||
|
title="Twitter Share"
|
||||||
|
data-aw-social-share="twitter"
|
||||||
|
data-aw-url={url}
|
||||||
|
data-aw-text={text}
|
||||||
|
><Icon
|
||||||
|
name="tabler:brand-x"
|
||||||
|
class="w-6 h-6 text-gray-400 dark:text-slate-500 hover:text-black dark:hover:text-slate-300"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button class="ml-2 rtl:ml-0 rtl:mr-2" title="Facebook Share" data-aw-social-share="facebook" data-aw-url={url}
|
||||||
|
><Icon
|
||||||
|
name="tabler:brand-facebook"
|
||||||
|
class="w-6 h-6 text-gray-400 dark:text-slate-500 hover:text-black dark:hover:text-slate-300"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="ml-2 rtl:ml-0 rtl:mr-2"
|
||||||
|
title="Linkedin Share"
|
||||||
|
data-aw-social-share="linkedin"
|
||||||
|
data-aw-url={url}
|
||||||
|
data-aw-text={text}
|
||||||
|
><Icon
|
||||||
|
name="tabler:brand-linkedin"
|
||||||
|
class="w-6 h-6 text-gray-400 dark:text-slate-500 hover:text-black dark:hover:text-slate-300"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="ml-2 rtl:ml-0 rtl:mr-2"
|
||||||
|
title="Whatsapp Share"
|
||||||
|
data-aw-social-share="whatsapp"
|
||||||
|
data-aw-url={url}
|
||||||
|
data-aw-text={text}
|
||||||
|
><Icon
|
||||||
|
name="tabler:brand-whatsapp"
|
||||||
|
class="w-6 h-6 text-gray-400 dark:text-slate-500 hover:text-black dark:hover:text-slate-300"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="ml-2 rtl:ml-0 rtl:mr-2"
|
||||||
|
title="Email Share"
|
||||||
|
data-aw-social-share="mail"
|
||||||
|
data-aw-url={url}
|
||||||
|
data-aw-text={text}
|
||||||
|
><Icon
|
||||||
|
name="tabler:mail"
|
||||||
|
class="w-6 h-6 text-gray-400 dark:text-slate-500 hover:text-black dark:hover:text-slate-300"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
6
v3/src/components/common/SplitbeeAnalytics.astro
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
const { doNotTrack = true, noCookieMode = false, url = 'https://cdn.splitbee.io/sb.js' } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Splitbee Analytics -->
|
||||||
|
<script is:inline data-respect-dnt={doNotTrack} data-no-cookie={noCookieMode} async src={url}></script>
|
29
v3/src/components/common/ToggleMenu.astro
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
export interface Props {
|
||||||
|
label?: string;
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
label = 'Toggle Menu',
|
||||||
|
class: className = 'flex flex-col h-12 w-12 rounded justify-center items-center cursor-pointer group',
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<button type="button" class={className} aria-label={label} data-aw-toggle-menu>
|
||||||
|
<span class="sr-only">{label}</span>
|
||||||
|
<slot>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="h-0.5 w-6 my-1 rounded-full bg-black dark:bg-white transition ease transform duration-200 opacity-80 group-[.expanded]:rotate-45 group-[.expanded]:translate-y-2.5"
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="h-0.5 w-6 my-1 rounded-full bg-black dark:bg-white transition ease transform duration-200 opacity-80 group-[.expanded]:opacity-0"
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="h-0.5 w-6 my-1 rounded-full bg-black dark:bg-white transition ease transform duration-200 opacity-80 group-[.expanded]:-rotate-45 group-[.expanded]:-translate-y-2.5"
|
||||||
|
></span>
|
||||||
|
</slot>
|
||||||
|
</button>
|
28
v3/src/components/common/ToggleTheme.astro
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
|
||||||
|
import { UI } from 'astrowind:config';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
label?: string;
|
||||||
|
class?: string;
|
||||||
|
iconClass?: string;
|
||||||
|
iconName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
label = 'Toggle between Dark and Light mode',
|
||||||
|
class:
|
||||||
|
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',
|
||||||
|
iconClass = 'w-6 h-6',
|
||||||
|
iconName = 'tabler:sun',
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
!(UI.theme && UI.theme.endsWith(':only')) && (
|
||||||
|
<button type="button" class={className} aria-label={label} data-aw-toggle-color-scheme>
|
||||||
|
<Icon name={iconName} class={iconClass} />
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
11
v3/src/components/ui/Background.astro
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
export interface Props {
|
||||||
|
isDark?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { isDark = false } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class:list={['absolute inset-0', { 'bg-dark dark:bg-transparent': isDark }]}>
|
||||||
|
<slot />
|
||||||
|
</div>
|
40
v3/src/components/ui/Button.astro
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import type { CallToAction as Props } from '~/types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
variant = 'secondary',
|
||||||
|
target,
|
||||||
|
text = Astro.slots.render('default'),
|
||||||
|
icon = '',
|
||||||
|
class: className = '',
|
||||||
|
type,
|
||||||
|
...rest
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const variants = {
|
||||||
|
primary: 'btn-primary',
|
||||||
|
secondary: 'btn-secondary',
|
||||||
|
tertiary: 'btn btn-tertiary',
|
||||||
|
link: 'cursor-pointer hover:text-primary',
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
type === 'button' || type === 'submit' || type === 'reset' ? (
|
||||||
|
<button type={type} class={twMerge(variants[variant] || '', className)} {...rest}>
|
||||||
|
<Fragment set:html={text} />
|
||||||
|
{icon && <Icon name={icon} class="w-5 h-5 ml-1 -mr-1.5 rtl:mr-1 rtl:-ml-1.5 inline-block" />}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
class={twMerge(variants[variant] || '', className)}
|
||||||
|
{...(target ? { target: target, rel: 'noopener noreferrer' } : {})}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<Fragment set:html={text} />
|
||||||
|
{icon && <Icon name={icon} class="w-5 h-5 ml-1 -mr-1.5 rtl:mr-1 rtl:-ml-1.5 inline-block" />}
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
22
v3/src/components/ui/DListItem.astro
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
// component: DListItem
|
||||||
|
//
|
||||||
|
// Mimics the html 'dl' (description list)
|
||||||
|
//
|
||||||
|
// The 'dt' item is the item 'term' and is inserted into an 'h6' tag.
|
||||||
|
// Caller needs to style the 'h6' tag appropriately.
|
||||||
|
//
|
||||||
|
// You can put pretty much any content you want between the open and
|
||||||
|
// closing tags - it's simply contained in an enclosing div with a
|
||||||
|
// margin left. No need for 'dd' items.
|
||||||
|
//
|
||||||
|
const { dt } = Astro.props;
|
||||||
|
interface Props {
|
||||||
|
dt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content: string = await Astro.slots.render('default');
|
||||||
|
---
|
||||||
|
|
||||||
|
<h6 set:html={dt} />
|
||||||
|
<div class="dd ml-8" set:html={content} />
|
87
v3/src/components/ui/Form.astro
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
import type { Form as Props } from '~/types';
|
||||||
|
import Button from '~/components/ui/Button.astro';
|
||||||
|
|
||||||
|
const { inputs, textarea, disclaimer, button = 'Contact us', description = '' } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<form>
|
||||||
|
{
|
||||||
|
inputs &&
|
||||||
|
inputs.map(
|
||||||
|
({ type = 'text', name, label = '', autocomplete = 'on', placeholder = '' }) =>
|
||||||
|
name && (
|
||||||
|
<div class="mb-6">
|
||||||
|
{label && (
|
||||||
|
<label for={name} class="block text-sm font-medium">
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
name={name}
|
||||||
|
id={name}
|
||||||
|
autocomplete={autocomplete}
|
||||||
|
placeholder={placeholder}
|
||||||
|
class="py-3 px-4 block w-full text-md rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
textarea && (
|
||||||
|
<div>
|
||||||
|
<label for="textarea" class="block text-sm font-medium">
|
||||||
|
{textarea.label}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="textarea"
|
||||||
|
name={textarea.name ? textarea.name : 'message'}
|
||||||
|
rows={textarea.rows ? textarea.rows : 4}
|
||||||
|
placeholder={textarea.placeholder}
|
||||||
|
class="py-3 px-4 block w-full text-md rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
disclaimer && (
|
||||||
|
<div class="mt-3 flex items-start">
|
||||||
|
<div class="flex mt-0.5">
|
||||||
|
<input
|
||||||
|
id="disclaimer"
|
||||||
|
name="disclaimer"
|
||||||
|
type="checkbox"
|
||||||
|
class="cursor-pointer mt-1 py-3 px-4 block w-full text-md rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<label for="disclaimer" class="cursor-pointer select-none text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{disclaimer.label}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
button && (
|
||||||
|
<div class="mt-10 grid">
|
||||||
|
<Button variant="primary" type="submit">
|
||||||
|
{button}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
description && (
|
||||||
|
<div class="mt-3 text-center">
|
||||||
|
<p class="text-sm text-gray-600 dark:text-gray-400">{description}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</form>
|
35
v3/src/components/ui/Headline.astro
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
import type { Headline as Props } from '~/types';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline,
|
||||||
|
classes = {},
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
container: containerClass = 'max-w-3xl',
|
||||||
|
title: titleClass = 'text-3xl md:text-4xl ',
|
||||||
|
subtitle: subtitleClass = 'text-xl',
|
||||||
|
} = classes;
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
(title || subtitle || tagline) && (
|
||||||
|
<div class={twMerge('mb-8 md:mx-auto md:mb-12 text-center', containerClass)}>
|
||||||
|
{tagline && (
|
||||||
|
<p class="text-base text-secondary dark:text-blue-200 font-bold tracking-wide uppercase" set:html={tagline} />
|
||||||
|
)}
|
||||||
|
{title && (
|
||||||
|
<h2
|
||||||
|
class={twMerge('font-bold leading-tighter tracking-tighter font-heading text-heading text-3xl', titleClass)}
|
||||||
|
set:html={title}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{subtitle && <p class={twMerge('mt-4 text-muted', subtitleClass)} set:html={subtitle} />}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
65
v3/src/components/ui/ItemGrid.astro
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
import type { ItemGrid as Props } from '~/types';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import Button from './Button.astro';
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
|
||||||
|
const { items = [], columns, defaultIcon = '', classes = {} } = Astro.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
container: containerClass = '',
|
||||||
|
panel: panelClass = '',
|
||||||
|
title: titleClass = '',
|
||||||
|
description: descriptionClass = '',
|
||||||
|
icon: defaultIconClass = 'text-primary',
|
||||||
|
action: actionClass = '',
|
||||||
|
} = classes;
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
items && (
|
||||||
|
<div
|
||||||
|
class={twMerge(
|
||||||
|
`grid mx-auto gap-8 md:gap-y-12 ${
|
||||||
|
columns === 4
|
||||||
|
? 'lg:grid-cols-4 md:grid-cols-3 sm:grid-cols-2'
|
||||||
|
: columns === 3
|
||||||
|
? 'lg:grid-cols-3 sm:grid-cols-2'
|
||||||
|
: columns === 2
|
||||||
|
? 'sm:grid-cols-2 '
|
||||||
|
: ''
|
||||||
|
}`,
|
||||||
|
containerClass
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{items.map(({ title, description, icon, callToAction, classes: itemClasses = {} }) => (
|
||||||
|
<div>
|
||||||
|
<div class={twMerge('flex flex-row max-w-md', panelClass, itemClasses?.panel)}>
|
||||||
|
<div class="flex justify-center">
|
||||||
|
{(icon || defaultIcon) && (
|
||||||
|
<Icon
|
||||||
|
name={icon || defaultIcon}
|
||||||
|
class={twMerge('w-7 h-7 mr-2 rtl:mr-0 rtl:ml-2', defaultIconClass, itemClasses?.icon)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div class="mt-0.5">
|
||||||
|
{title && <h3 class={twMerge('text-xl font-bold', titleClass, itemClasses?.title)}>{title}</h3>}
|
||||||
|
{description && (
|
||||||
|
<p
|
||||||
|
class={twMerge(`${title ? 'mt-3' : ''} text-muted`, descriptionClass, itemClasses?.description)}
|
||||||
|
set:html={description}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{callToAction && (
|
||||||
|
<div class={twMerge(`${title || description ? 'mt-3' : ''}`, actionClass, itemClasses?.actionClass)}>
|
||||||
|
<Button variant="link" {...callToAction} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
53
v3/src/components/ui/ItemGrid2.astro
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
import type { ItemGrid as Props } from '~/types';
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import Button from './Button.astro';
|
||||||
|
|
||||||
|
const { items = [], columns, defaultIcon = '', classes = {} } = Astro.props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
container: containerClass = '',
|
||||||
|
// container: containerClass = "sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3",
|
||||||
|
panel: panelClass = '',
|
||||||
|
title: titleClass = '',
|
||||||
|
description: descriptionClass = '',
|
||||||
|
icon: defaultIconClass = 'text-primary',
|
||||||
|
} = classes;
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
items && (
|
||||||
|
<div
|
||||||
|
class={twMerge(
|
||||||
|
`grid gap-8 gap-x-12 sm:gap-y-8 ${
|
||||||
|
columns === 4
|
||||||
|
? 'lg:grid-cols-4 md:grid-cols-3 sm:grid-cols-2'
|
||||||
|
: columns === 3
|
||||||
|
? 'lg:grid-cols-3 sm:grid-cols-2'
|
||||||
|
: columns === 2
|
||||||
|
? 'sm:grid-cols-2 '
|
||||||
|
: ''
|
||||||
|
}`,
|
||||||
|
containerClass
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{items.map(({ title, description, icon, callToAction, classes: itemClasses = {} }) => (
|
||||||
|
<div class={twMerge('relative flex flex-col', panelClass, itemClasses?.panel)}>
|
||||||
|
{(icon || defaultIcon) && (
|
||||||
|
<Icon name={icon || defaultIcon} class={twMerge('mb-2 w-10 h-10', defaultIconClass, itemClasses?.icon)} />
|
||||||
|
)}
|
||||||
|
<div class={twMerge('text-xl font-bold', titleClass, itemClasses?.title)}>{title}</div>
|
||||||
|
{description && (
|
||||||
|
<p class={twMerge('text-muted mt-2', descriptionClass, itemClasses?.description)} set:html={description} />
|
||||||
|
)}
|
||||||
|
{callToAction && (
|
||||||
|
<div class="mt-2">
|
||||||
|
<Button {...callToAction} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
54
v3/src/components/ui/Timeline.astro
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import type { Item } from '~/types';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
items?: Array<Item>;
|
||||||
|
defaultIcon?: string;
|
||||||
|
classes?: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { items = [], classes = {}, defaultIcon } = Astro.props as Props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
container: containerClass = '',
|
||||||
|
panel: panelClass = '',
|
||||||
|
title: titleClass = '',
|
||||||
|
description: descriptionClass = '',
|
||||||
|
icon: defaultIconClass = 'text-primary dark:text-slate-200 border-primary dark:border-blue-700',
|
||||||
|
} = classes;
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
items && items.length && (
|
||||||
|
<div class={containerClass}>
|
||||||
|
{items.map(({ title, description, icon, classes: itemClasses = {} }, index = 0) => (
|
||||||
|
<div class={twMerge('flex', panelClass, itemClasses?.panel)}>
|
||||||
|
<div class="flex flex-col items-center mr-4 rtl:mr-0 rtl:ml-4">
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
{(icon || defaultIcon) && (
|
||||||
|
<Icon
|
||||||
|
name={icon || defaultIcon}
|
||||||
|
class={twMerge('w-10 h-10 p-2 rounded-full border-2', defaultIconClass, itemClasses?.icon)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{index !== items.length - 1 && <div class="w-px h-full bg-black/10 dark:bg-slate-400/50" />}
|
||||||
|
</div>
|
||||||
|
<div class={`pt-1 ${index !== items.length - 1 ? 'pb-8' : ''}`}>
|
||||||
|
{title && <p class={twMerge('text-xl font-bold', titleClass, itemClasses?.title)} set:html={title} />}
|
||||||
|
{description && (
|
||||||
|
<p
|
||||||
|
class={twMerge('text-muted mt-2', descriptionClass, itemClasses?.description)}
|
||||||
|
set:html={description}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
31
v3/src/components/ui/WidgetWrapper.astro
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
import type { HTMLTag } from 'astro/types';
|
||||||
|
import type { Widget } from '~/types';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
import Background from './Background.astro';
|
||||||
|
|
||||||
|
export interface Props extends Widget {
|
||||||
|
containerClass?: string;
|
||||||
|
['as']?: HTMLTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id, isDark = false, containerClass = '', bg, as = 'section' } = Astro.props;
|
||||||
|
|
||||||
|
const WrapperTag = as;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WrapperTag class="relative not-prose scroll-mt-[72px]" {...id ? { id } : {}}>
|
||||||
|
<div class="absolute inset-0 pointer-events-none -z-[1]" aria-hidden="true">
|
||||||
|
<slot name="bg">
|
||||||
|
{bg ? <Fragment set:html={bg} /> : <Background isDark={isDark} />}
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class:list={[
|
||||||
|
twMerge('relative mx-auto max-w-7xl px-4 md:px-6 py-12 md:py-16 lg:py-20 text-default', containerClass),
|
||||||
|
{ dark: isDark },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</WrapperTag>
|
23
v3/src/components/widgets/Announcement.astro
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="dark text-muted text-sm bg-black dark:bg-transparent dark:border-b dark:border-slate-800 dark:text-slate-400 hidden md:flex gap-1 overflow-hidden px-3 py-2 relative text-ellipsis whitespace-nowrap"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="dark:bg-slate-700 bg-white/40 dark:text-slate-300 font-semibold px-1 py-0.5 text-xs mr-0.5 rtl:mr-0 rtl:ml-0.5 inline-block"
|
||||||
|
>NEW</span
|
||||||
|
>
|
||||||
|
<a href="https://astro.build/blog/astro-4150/" class="text-muted hover:underline dark:text-slate-400 font-medium"
|
||||||
|
>Astro 4.15 is now available! »</a
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener"
|
||||||
|
class="ltr:ml-auto rtl:mr-auto w-[5.6rem] h-[1.25rem] ml-auto bg-contain inline-block bg-[url(https://img.shields.io/github/stars/onwidget/astrowind.svg?style=social&label=Stars&maxAge=86400)]"
|
||||||
|
title="If you like AstroWind, give us a star."
|
||||||
|
href="https://github.com/onwidget/astrowind"
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
</div>
|
64
v3/src/components/widgets/BlogHighlightedPosts.astro
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
import { APP_BLOG } from 'astrowind:config';
|
||||||
|
|
||||||
|
import Grid from '~/components/blog/Grid.astro';
|
||||||
|
|
||||||
|
import { getBlogPermalink } from '~/utils/permalinks';
|
||||||
|
import { findPostsByIds } from '~/utils/blog';
|
||||||
|
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||||
|
import type { Widget } from '~/types';
|
||||||
|
|
||||||
|
export interface Props extends Widget {
|
||||||
|
title?: string;
|
||||||
|
linkText?: string;
|
||||||
|
linkUrl?: string | URL;
|
||||||
|
information?: string;
|
||||||
|
postIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
linkText = 'View all posts',
|
||||||
|
linkUrl = getBlogPermalink(),
|
||||||
|
information = await Astro.slots.render('information'),
|
||||||
|
postIds = [],
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const posts = APP_BLOG.isEnabled ? await findPostsByIds(postIds) : [];
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
APP_BLOG.isEnabled ? (
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={classes?.container as string} bg={bg}>
|
||||||
|
<div class="flex flex-col lg:justify-between lg:flex-row mb-8">
|
||||||
|
{title && (
|
||||||
|
<div class="md:max-w-sm">
|
||||||
|
<h2
|
||||||
|
class="text-3xl font-bold tracking-tight sm:text-4xl sm:leading-none group font-heading mb-2"
|
||||||
|
set:html={title}
|
||||||
|
/>
|
||||||
|
{APP_BLOG.list.isEnabled && linkText && linkUrl && (
|
||||||
|
<a
|
||||||
|
class="text-muted dark:text-slate-400 hover:text-primary transition ease-in duration-200 block mb-6 lg:mb-0"
|
||||||
|
href={linkUrl}
|
||||||
|
>
|
||||||
|
{linkText} »
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{information && <p class="text-muted dark:text-slate-400 lg:text-sm lg:max-w-md" set:html={information} />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Grid posts={posts} />
|
||||||
|
</WidgetWrapper>
|
||||||
|
) : (
|
||||||
|
<Fragment />
|
||||||
|
)
|
||||||
|
}
|
63
v3/src/components/widgets/BlogLatestPosts.astro
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
import { APP_BLOG } from 'astrowind:config';
|
||||||
|
|
||||||
|
import Grid from '~/components/blog/Grid.astro';
|
||||||
|
|
||||||
|
import { getBlogPermalink } from '~/utils/permalinks';
|
||||||
|
import { findLatestPosts } from '~/utils/blog';
|
||||||
|
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||||
|
import type { Widget } from '~/types';
|
||||||
|
import Button from '../ui/Button.astro';
|
||||||
|
|
||||||
|
export interface Props extends Widget {
|
||||||
|
title?: string;
|
||||||
|
linkText?: string;
|
||||||
|
linkUrl?: string | URL;
|
||||||
|
information?: string;
|
||||||
|
count?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
linkText = 'View all posts',
|
||||||
|
linkUrl = getBlogPermalink(),
|
||||||
|
information = await Astro.slots.render('information'),
|
||||||
|
count = 4,
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const posts = APP_BLOG.isEnabled ? await findLatestPosts({ count }) : [];
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
APP_BLOG.isEnabled ? (
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={classes?.container as string} bg={bg}>
|
||||||
|
<div class="flex flex-col lg:justify-between lg:flex-row mb-8">
|
||||||
|
{title && (
|
||||||
|
<div class="md:max-w-sm">
|
||||||
|
<h2
|
||||||
|
class="text-3xl font-bold tracking-tight sm:text-4xl sm:leading-none group font-heading mb-2"
|
||||||
|
set:html={title}
|
||||||
|
/>
|
||||||
|
{APP_BLOG.list.isEnabled && linkText && linkUrl && (
|
||||||
|
<Button variant="link" href={linkUrl}>
|
||||||
|
{' '}
|
||||||
|
{linkText} »
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{information && <p class="text-muted dark:text-slate-400 lg:text-sm lg:max-w-md" set:html={information} />}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Grid posts={posts} />
|
||||||
|
</WidgetWrapper>
|
||||||
|
) : (
|
||||||
|
<Fragment />
|
||||||
|
)
|
||||||
|
}
|
38
v3/src/components/widgets/Brands.astro
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import type { Brands as Props } from '~/types';
|
||||||
|
|
||||||
|
import Image from '~/components/common/Image.astro';
|
||||||
|
import Headline from '~/components/ui/Headline.astro';
|
||||||
|
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||||
|
const {
|
||||||
|
title = '',
|
||||||
|
subtitle = '',
|
||||||
|
tagline = '',
|
||||||
|
icons = [],
|
||||||
|
images = [],
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-6xl mx-auto ${classes?.container ?? ''}`} bg={bg}>
|
||||||
|
<Headline title={title} subtitle={subtitle} tagline={tagline} />
|
||||||
|
|
||||||
|
<div class="flex flex-wrap justify-center gap-x-6 sm:gap-x-12 lg:gap-x-24">
|
||||||
|
{icons && icons.map((icon) => <Icon name={icon} class="py-3 lg:py-5 w-12 h-auto mx-auto sm:mx-0 text-gray-500" />)}
|
||||||
|
{
|
||||||
|
images &&
|
||||||
|
images.map(
|
||||||
|
(image) =>
|
||||||
|
image.src && (
|
||||||
|
<div class="flex justify-center col-span-1 my-2 lg:my-4 py-1 px-3 rounded-md dark:bg-gray-200">
|
||||||
|
<Image src={image.src} alt={image.alt || ''} class="max-h-12" width={120} height={48} layout="fixed" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</WidgetWrapper>
|
58
v3/src/components/widgets/CallToAction.astro
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
---
|
||||||
|
import WidgetWrapper from '../ui/WidgetWrapper.astro';
|
||||||
|
import type { CallToAction, Widget } from '~/types';
|
||||||
|
import Headline from '~/components/ui/Headline.astro';
|
||||||
|
import Button from '~/components/ui/Button.astro';
|
||||||
|
|
||||||
|
interface Props extends Widget {
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
tagline?: string;
|
||||||
|
callToAction?: CallToAction;
|
||||||
|
actions?: string | CallToAction[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline = await Astro.slots.render('tagline'),
|
||||||
|
actions = await Astro.slots.render('actions'),
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-6xl mx-auto ${classes?.container ?? ''}`} bg={bg}>
|
||||||
|
<div
|
||||||
|
class="max-w-3xl mx-auto text-center p-6 rounded-md shadow-xl dark:shadow-none dark:border dark:border-slate-600"
|
||||||
|
>
|
||||||
|
<Headline
|
||||||
|
title={title}
|
||||||
|
subtitle={subtitle}
|
||||||
|
tagline={tagline}
|
||||||
|
classes={{
|
||||||
|
container: 'mb-0 md:mb-0',
|
||||||
|
title: 'text-4xl md:text-4xl font-bold tracking-tighter mb-4 font-heading',
|
||||||
|
subtitle: 'text-xl text-muted dark:text-slate-400',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
actions && (
|
||||||
|
<div class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4 mt-6">
|
||||||
|
{Array.isArray(actions) ? (
|
||||||
|
actions.map((action) => (
|
||||||
|
<div class="flex w-full sm:w-auto">
|
||||||
|
<Button {...(action || {})} class="w-full sm:mb-0" />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Fragment set:html={actions} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</WidgetWrapper>
|
40
v3/src/components/widgets/Contact.astro
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
import FormContainer from '~/components/ui/Form.astro';
|
||||||
|
import Headline from '~/components/ui/Headline.astro';
|
||||||
|
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||||
|
import type { Contact as Props } from '~/types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline = await Astro.slots.render('tagline'),
|
||||||
|
inputs,
|
||||||
|
textarea,
|
||||||
|
disclaimer,
|
||||||
|
button,
|
||||||
|
description,
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-7xl mx-auto ${classes?.container ?? ''}`} bg={bg}>
|
||||||
|
<Headline title={title} subtitle={subtitle} tagline={tagline} />
|
||||||
|
|
||||||
|
{
|
||||||
|
inputs && (
|
||||||
|
<div class="flex flex-col max-w-xl mx-auto rounded-lg backdrop-blur border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900 shadow p-4 sm:p-6 lg:p-8 w-full">
|
||||||
|
<FormContainer
|
||||||
|
inputs={inputs}
|
||||||
|
textarea={textarea}
|
||||||
|
disclaimer={disclaimer}
|
||||||
|
button={button}
|
||||||
|
description={description}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</WidgetWrapper>
|
94
v3/src/components/widgets/Content.astro
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
---
|
||||||
|
import type { Content as Props } from '~/types';
|
||||||
|
import Headline from '../ui/Headline.astro';
|
||||||
|
import WidgetWrapper from '../ui/WidgetWrapper.astro';
|
||||||
|
import Image from '~/components/common/Image.astro';
|
||||||
|
import Button from '~/components/ui/Button.astro';
|
||||||
|
import ItemGrid from '../ui/ItemGrid.astro';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline,
|
||||||
|
content = await Astro.slots.render('content'),
|
||||||
|
callToAction,
|
||||||
|
items = [],
|
||||||
|
columns,
|
||||||
|
image = await Astro.slots.render('image'),
|
||||||
|
isReversed = false,
|
||||||
|
isAfterContent = false,
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper
|
||||||
|
id={id}
|
||||||
|
isDark={isDark}
|
||||||
|
containerClass={`max-w-7xl mx-auto ${isAfterContent ? 'pt-0 md:pt-0 lg:pt-0' : ''} ${classes?.container ?? ''}`}
|
||||||
|
bg={bg}
|
||||||
|
>
|
||||||
|
<Headline
|
||||||
|
title={title}
|
||||||
|
subtitle={subtitle}
|
||||||
|
tagline={tagline}
|
||||||
|
classes={{
|
||||||
|
container: 'max-w-xl sm:mx-auto lg:max-w-2xl',
|
||||||
|
title: 'text-4xl md:text-5xl font-bold tracking-tighter mb-4 font-heading',
|
||||||
|
subtitle: 'max-w-3xl mx-auto sm:text-center text-xl text-muted dark:text-slate-400',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div class="mx-auto max-w-7xl p-4 md:px-8">
|
||||||
|
<div class={`md:flex ${isReversed ? 'md:flex-row-reverse' : ''} md:gap-16`}>
|
||||||
|
<div class="md:basis-1/2 self-center">
|
||||||
|
{content && <div class="mb-12 text-lg dark:text-slate-400" set:html={content} />}
|
||||||
|
|
||||||
|
{
|
||||||
|
callToAction && (
|
||||||
|
<div class="mt-[-40px] mb-8 text-primary">
|
||||||
|
<Button variant="link" {...callToAction} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<ItemGrid
|
||||||
|
items={items}
|
||||||
|
columns={columns}
|
||||||
|
defaultIcon="tabler:check"
|
||||||
|
classes={{
|
||||||
|
container: `gap-y-4 md:gap-y-8`,
|
||||||
|
panel: 'max-w-none',
|
||||||
|
title: 'text-lg font-medium leading-6 dark:text-white ml-2 rtl:ml-0 rtl:mr-2',
|
||||||
|
description: 'text-muted dark:text-slate-400 ml-2 rtl:ml-0 rtl:mr-2',
|
||||||
|
icon: 'flex h-7 w-7 items-center justify-center rounded-full bg-green-600 dark:bg-green-700 text-gray-50 p-1',
|
||||||
|
action: 'text-lg font-medium leading-6 dark:text-white ml-2 rtl:ml-0 rtl:mr-2',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div aria-hidden="true" class="mt-10 md:mt-0 md:basis-1/2">
|
||||||
|
{
|
||||||
|
image && (
|
||||||
|
<div class="relative m-auto max-w-4xl">
|
||||||
|
{typeof image === 'string' ? (
|
||||||
|
<Fragment set:html={image} />
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
class="mx-auto w-full rounded-lg bg-gray-500 shadow-lg"
|
||||||
|
width={500}
|
||||||
|
height={500}
|
||||||
|
widths={[400, 768]}
|
||||||
|
sizes="(max-width: 768px) 100vw, 432px"
|
||||||
|
layout="responsive"
|
||||||
|
{...image}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</WidgetWrapper>
|
33
v3/src/components/widgets/FAQs.astro
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
import Headline from '~/components/ui/Headline.astro';
|
||||||
|
import ItemGrid from '~/components/ui/ItemGrid.astro';
|
||||||
|
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||||
|
import type { Faqs as Props } from '~/types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = '',
|
||||||
|
subtitle = '',
|
||||||
|
tagline = '',
|
||||||
|
items = [],
|
||||||
|
columns = 2,
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-7xl mx-auto ${classes?.container ?? ''}`} bg={bg}>
|
||||||
|
<Headline title={title} subtitle={subtitle} tagline={tagline} />
|
||||||
|
<ItemGrid
|
||||||
|
items={items}
|
||||||
|
columns={columns}
|
||||||
|
defaultIcon="tabler:chevron-right"
|
||||||
|
classes={{
|
||||||
|
container: `${columns === 1 ? 'max-w-4xl' : ''} gap-y-8 md:gap-y-12`,
|
||||||
|
panel: 'max-w-none',
|
||||||
|
icon: 'flex-shrink-0 mt-1 w-6 h-6 text-primary',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</WidgetWrapper>
|
36
v3/src/components/widgets/Features.astro
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||||
|
import ItemGrid from '~/components/ui/ItemGrid.astro';
|
||||||
|
import Headline from '~/components/ui/Headline.astro';
|
||||||
|
import type { Features as Props } from '~/types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline = await Astro.slots.render('tagline'),
|
||||||
|
items = [],
|
||||||
|
columns = 2,
|
||||||
|
|
||||||
|
defaultIcon,
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-5xl ${classes?.container ?? ''}`} bg={bg}>
|
||||||
|
<Headline title={title} subtitle={subtitle} tagline={tagline} classes={classes?.headline as Record<string, string>} />
|
||||||
|
<ItemGrid
|
||||||
|
items={items}
|
||||||
|
columns={columns}
|
||||||
|
defaultIcon={defaultIcon}
|
||||||
|
classes={{
|
||||||
|
container: '',
|
||||||
|
title: 'md:text-[1.3rem]',
|
||||||
|
icon: 'text-white bg-primary rounded-full w-10 h-10 p-2 md:w-12 md:h-12 md:p-3 mr-4 rtl:ml-4 rtl:mr-0',
|
||||||
|
...((classes?.items as Record<string, never>) ?? {}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</WidgetWrapper>
|
38
v3/src/components/widgets/Features2.astro
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||||
|
import Headline from '~/components/ui/Headline.astro';
|
||||||
|
import ItemGrid2 from '~/components/ui/ItemGrid2.astro';
|
||||||
|
import type { Features as Props } from '~/types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline = await Astro.slots.render('tagline'),
|
||||||
|
items = [],
|
||||||
|
columns = 3,
|
||||||
|
defaultIcon,
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-7xl mx-auto ${classes?.container ?? ''}`} bg={bg}>
|
||||||
|
<Headline title={title} subtitle={subtitle} tagline={tagline} classes={classes?.headline as Record<string, string>} />
|
||||||
|
<ItemGrid2
|
||||||
|
items={items}
|
||||||
|
columns={columns}
|
||||||
|
defaultIcon={defaultIcon}
|
||||||
|
classes={{
|
||||||
|
container: 'gap-4 md:gap-6',
|
||||||
|
panel:
|
||||||
|
'rounded-lg shadow-[0_4px_30px_rgba(0,0,0,0.1)] dark:shadow-[0_4px_30px_rgba(0,0,0,0.1)] backdrop-blur border border-[#ffffff29] bg-white dark:bg-slate-900 p-6',
|
||||||
|
// panel:
|
||||||
|
// "text-center bg-page items-center md:text-left rtl:md:text-right md:items-start p-6 p-6 rounded-md shadow-xl dark:shadow-none dark:border dark:border-slate-800",
|
||||||
|
icon: 'w-12 h-12 mb-6 text-primary',
|
||||||
|
...((classes?.items as Record<string, never>) ?? {}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</WidgetWrapper>
|
70
v3/src/components/widgets/Features3.astro
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
import Headline from '~/components/ui/Headline.astro';
|
||||||
|
import ItemGrid from '~/components/ui/ItemGrid.astro';
|
||||||
|
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||||
|
import Image from '~/components/common/Image.astro';
|
||||||
|
import type { Features as Props } from '~/types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline = await Astro.slots.render('tagline'),
|
||||||
|
image,
|
||||||
|
items = [],
|
||||||
|
columns,
|
||||||
|
defaultIcon,
|
||||||
|
isBeforeContent,
|
||||||
|
isAfterContent,
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper
|
||||||
|
id={id}
|
||||||
|
isDark={isDark}
|
||||||
|
containerClass={`${isBeforeContent ? 'md:pb-8 lg:pb-12' : ''} ${isAfterContent ? 'pt-0 md:pt-0 lg:pt-0' : ''} ${
|
||||||
|
classes?.container ?? ''
|
||||||
|
}`}
|
||||||
|
bg={bg}
|
||||||
|
>
|
||||||
|
<Headline title={title} subtitle={subtitle} tagline={tagline} classes={classes?.headline as Record<string, string>} />
|
||||||
|
|
||||||
|
<div aria-hidden="true" class="aspect-w-16 aspect-h-7">
|
||||||
|
{
|
||||||
|
image && (
|
||||||
|
<div class="w-full h-80 object-cover rounded-xl mx-auto bg-gray-500 shadow-lg">
|
||||||
|
{typeof image === 'string' ? (
|
||||||
|
<Fragment set:html={image} />
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
class="w-full h-80 object-cover rounded-xl mx-auto bg-gray-500 shadow-lg"
|
||||||
|
width="auto"
|
||||||
|
height={320}
|
||||||
|
widths={[400, 768]}
|
||||||
|
layout="fullWidth"
|
||||||
|
{...image}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ItemGrid
|
||||||
|
items={items}
|
||||||
|
columns={columns}
|
||||||
|
defaultIcon={defaultIcon}
|
||||||
|
classes={{
|
||||||
|
container: 'mt-12',
|
||||||
|
panel: 'max-w-full sm:max-w-md',
|
||||||
|
title: 'text-lg font-semibold',
|
||||||
|
description: 'mt-0.5',
|
||||||
|
icon: 'flex-shrink-0 mt-1 text-primary w-6 h-6',
|
||||||
|
...((classes?.items as object) ?? {}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</WidgetWrapper>
|
102
v3/src/components/widgets/Footer.astro
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import { SITE } from 'astrowind:config';
|
||||||
|
import { getHomePermalink } from '~/utils/permalinks';
|
||||||
|
|
||||||
|
interface Link {
|
||||||
|
text?: string;
|
||||||
|
href?: string;
|
||||||
|
ariaLabel?: string;
|
||||||
|
icon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Links {
|
||||||
|
title?: string;
|
||||||
|
links: Array<Link>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
links: Array<Links>;
|
||||||
|
secondaryLinks: Array<Link>;
|
||||||
|
socialLinks: Array<Link>;
|
||||||
|
footNote?: string;
|
||||||
|
theme?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { socialLinks = [], secondaryLinks = [], links = [], footNote = '', theme = 'light' } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<footer class:list={[{ dark: theme === 'dark' }, 'relative border-t border-gray-200 dark:border-slate-800 not-prose']}>
|
||||||
|
<div class="dark:bg-dark absolute inset-0 pointer-events-none" aria-hidden="true"></div>
|
||||||
|
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 dark:text-slate-300">
|
||||||
|
<div class="grid grid-cols-12 gap-4 gap-y-8 sm:gap-8 py-8 md:py-12">
|
||||||
|
<div class="col-span-12 lg:col-span-4">
|
||||||
|
<div class="mb-2">
|
||||||
|
<a class="inline-block font-bold text-xl" href={getHomePermalink()}>{SITE?.name}</a>
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-muted flex gap-1">
|
||||||
|
{
|
||||||
|
secondaryLinks.map(({ text, href }, index) => (
|
||||||
|
<>
|
||||||
|
{index !== 0 ? ' · ' : ''}
|
||||||
|
<a
|
||||||
|
class="text-muted hover:text-gray-700 dark:text-gray-400 hover:underline transition duration-150 ease-in-out"
|
||||||
|
href={href}
|
||||||
|
set:html={text}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
links.map(({ title, links }) => (
|
||||||
|
<div class="col-span-6 md:col-span-3 lg:col-span-2">
|
||||||
|
<div class="dark:text-gray-300 font-medium mb-2">{title}</div>
|
||||||
|
{links && Array.isArray(links) && links.length > 0 && (
|
||||||
|
<ul class="text-sm">
|
||||||
|
{links.map(({ text, href, ariaLabel }) => (
|
||||||
|
<li class="mb-2">
|
||||||
|
<a
|
||||||
|
class="text-muted hover:text-gray-700 hover:underline dark:text-gray-400 transition duration-150 ease-in-out"
|
||||||
|
href={href}
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
>
|
||||||
|
<Fragment set:html={text} />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="md:flex md:items-center md:justify-between py-6 md:py-8">
|
||||||
|
{
|
||||||
|
socialLinks?.length ? (
|
||||||
|
<ul class="flex mb-4 md:order-1 -ml-2 md:ml-4 md:mb-0 rtl:ml-0 rtl:-mr-2 rtl:md:ml-0 rtl:md:mr-4">
|
||||||
|
{socialLinks.map(({ ariaLabel, href, text, icon }) => (
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class="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"
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
href={href}
|
||||||
|
>
|
||||||
|
{icon && <Icon name={icon} class="w-5 h-5" />}
|
||||||
|
<Fragment set:html={text} />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="text-sm mr-4 dark:text-muted">
|
||||||
|
<Fragment set:html={footNote} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
164
v3/src/components/widgets/Header.astro
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import Logo from '~/components/Logo.astro';
|
||||||
|
import ToggleTheme from '~/components/common/ToggleTheme.astro';
|
||||||
|
import ToggleMenu from '~/components/common/ToggleMenu.astro';
|
||||||
|
import Button from '~/components/ui/Button.astro';
|
||||||
|
|
||||||
|
import { getHomePermalink } from '~/utils/permalinks';
|
||||||
|
import { trimSlash, getAsset } from '~/utils/permalinks';
|
||||||
|
import type { CallToAction } from '~/types';
|
||||||
|
|
||||||
|
interface Link {
|
||||||
|
text?: string;
|
||||||
|
href?: string;
|
||||||
|
ariaLabel?: string;
|
||||||
|
icon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MenuLink extends Link {
|
||||||
|
links?: Array<MenuLink>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
id?: string;
|
||||||
|
links?: Array<MenuLink>;
|
||||||
|
actions?: Array<CallToAction>;
|
||||||
|
isSticky?: boolean;
|
||||||
|
isDark?: boolean;
|
||||||
|
isFullWidth?: boolean;
|
||||||
|
showToggleTheme?: boolean;
|
||||||
|
showRssFeed?: boolean;
|
||||||
|
position?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
id = 'header',
|
||||||
|
links = [],
|
||||||
|
actions = [],
|
||||||
|
isSticky = false,
|
||||||
|
isDark = false,
|
||||||
|
isFullWidth = false,
|
||||||
|
showToggleTheme = false,
|
||||||
|
showRssFeed = false,
|
||||||
|
position = 'center',
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const currentPath = `/${trimSlash(new URL(Astro.url).pathname)}`;
|
||||||
|
---
|
||||||
|
|
||||||
|
<header
|
||||||
|
class:list={[
|
||||||
|
{ sticky: isSticky, relative: !isSticky, dark: isDark },
|
||||||
|
'top-0 z-40 flex-none mx-auto w-full border-b border-gray-50/0 transition-[opacity] ease-in-out',
|
||||||
|
]}
|
||||||
|
{...isSticky ? { 'data-aw-sticky-header': true } : {}}
|
||||||
|
{...id ? { id } : {}}
|
||||||
|
>
|
||||||
|
<div class="absolute inset-0"></div>
|
||||||
|
<div
|
||||||
|
class:list={[
|
||||||
|
'relative text-default py-3 px-3 md:px-6 mx-auto w-full',
|
||||||
|
{
|
||||||
|
'md:flex md:justify-between': position !== 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'md:grid md:grid-cols-3 md:items-center': position === 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'max-w-7xl': !isFullWidth,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div class:list={[{ 'mr-auto rtl:mr-0 rtl:ml-auto': position === 'right' }, 'flex justify-between']}>
|
||||||
|
<a class="flex items-center" href={getHomePermalink()}>
|
||||||
|
<Logo />
|
||||||
|
</a>
|
||||||
|
<div class="flex items-center md:hidden">
|
||||||
|
<ToggleMenu />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav
|
||||||
|
class="items-center w-full md:w-auto hidden md:flex md:mx-5 text-default overflow-y-auto overflow-x-hidden md:overflow-y-visible md:overflow-x-auto md:justify-self-center"
|
||||||
|
aria-label="Main navigation"
|
||||||
|
>
|
||||||
|
<ul
|
||||||
|
class="flex flex-col md:flex-row md:self-center w-full md:w-auto text-xl md:text-[0.9375rem] tracking-[0.01rem] font-medium md:justify-center"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
links.map(({ text, href, links }) => (
|
||||||
|
<li class={links?.length ? 'dropdown' : ''}>
|
||||||
|
{links?.length ? (
|
||||||
|
<>
|
||||||
|
<button type="button" class="hover:text-link dark:hover:text-white px-4 py-3 flex items-center">
|
||||||
|
{text}{' '}
|
||||||
|
<Icon name="tabler:chevron-down" class="w-3.5 h-3.5 ml-0.5 rtl:ml-0 rtl:mr-0.5 hidden md:inline" />
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu md:backdrop-blur-md dark:md:bg-dark rounded md:absolute pl-4 md:pl-0 md:hidden font-medium md:bg-white/90 md:min-w-[200px] drop-shadow-xl">
|
||||||
|
{links.map(({ text: text2, href: href2 }) => (
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
class:list={[
|
||||||
|
'first:rounded-t last:rounded-b md:hover:bg-gray-100 hover:text-link dark:hover:text-white dark:hover:bg-gray-700 py-2 px-5 block whitespace-no-wrap',
|
||||||
|
{ 'aw-link-active': href2 === currentPath },
|
||||||
|
]}
|
||||||
|
href={href2}
|
||||||
|
>
|
||||||
|
{text2}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
class:list={[
|
||||||
|
'hover:text-link dark:hover:text-white px-4 py-3 flex items-center',
|
||||||
|
{ 'aw-link-active': href === currentPath },
|
||||||
|
]}
|
||||||
|
href={href}
|
||||||
|
>
|
||||||
|
{text}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<div
|
||||||
|
class:list={[
|
||||||
|
{ 'ml-auto rtl:ml-0 rtl:mr-auto': position === 'left' },
|
||||||
|
'hidden md:self-center md:flex items-center md:mb-0 fixed w-full md:w-auto md:static justify-end left-0 rtl:left-auto rtl:right-0 bottom-0 p-3 md:p-0 md:justify-self-end',
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<div class="items-center flex justify-between w-full md:w-auto">
|
||||||
|
<div class="flex">
|
||||||
|
{showToggleTheme && <ToggleTheme iconClass="w-6 h-6 md:w-5 md:h-5 md:inline-block" />}
|
||||||
|
{
|
||||||
|
showRssFeed && (
|
||||||
|
<a
|
||||||
|
class="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"
|
||||||
|
aria-label="RSS Feed"
|
||||||
|
href={getAsset('/rss.xml')}
|
||||||
|
>
|
||||||
|
<Icon name="tabler:rss" class="w-5 h-5" />
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
actions?.length ? (
|
||||||
|
<span class="ml-4 rtl:ml-0 rtl:mr-4">
|
||||||
|
{actions.map((btnProps) => (
|
||||||
|
<Button {...btnProps} class="ml-2 py-2.5 px-5.5 md:px-6 font-semibold shadow-none text-sm w-auto" />
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
90
v3/src/components/widgets/Hero.astro
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
---
|
||||||
|
import Image from '~/components/common/Image.astro';
|
||||||
|
import Button from '~/components/ui/Button.astro';
|
||||||
|
|
||||||
|
import type { Hero as Props } from '~/types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline,
|
||||||
|
|
||||||
|
content = await Astro.slots.render('content'),
|
||||||
|
actions = await Astro.slots.render('actions'),
|
||||||
|
image = await Astro.slots.render('image'),
|
||||||
|
|
||||||
|
id,
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="relative md:-mt-[76px] not-prose" {...id ? { id } : {}}>
|
||||||
|
<div class="absolute inset-0 pointer-events-none" aria-hidden="true">
|
||||||
|
<slot name="bg">
|
||||||
|
{bg ? <Fragment set:html={bg} /> : null}
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div class="relative max-w-7xl mx-auto px-4 sm:px-6">
|
||||||
|
<div class="pt-0 md:pt-[76px] pointer-events-none"></div>
|
||||||
|
<div class="py-12 md:py-20">
|
||||||
|
<div class="text-center pb-10 md:pb-16 max-w-5xl mx-auto">
|
||||||
|
{
|
||||||
|
tagline && (
|
||||||
|
<p
|
||||||
|
class="text-base text-secondary dark:text-blue-200 font-bold tracking-wide uppercase"
|
||||||
|
set:html={tagline}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
title && (
|
||||||
|
<h1
|
||||||
|
class="text-5xl md:text-6xl font-bold leading-tighter tracking-tighter mb-4 font-heading dark:text-gray-200"
|
||||||
|
set:html={title}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<div class="max-w-3xl mx-auto">
|
||||||
|
{subtitle && <p class="text-xl text-muted mb-6 dark:text-slate-300" set:html={subtitle} />}
|
||||||
|
{
|
||||||
|
actions && (
|
||||||
|
<div class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4">
|
||||||
|
{Array.isArray(actions) ? (
|
||||||
|
actions.map((action) => (
|
||||||
|
<div class="flex w-full sm:w-auto">
|
||||||
|
<Button {...(action || {})} class="w-full sm:mb-0" />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Fragment set:html={actions} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{content && <Fragment set:html={content} />}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{
|
||||||
|
image && (
|
||||||
|
<div class="relative m-auto max-w-5xl">
|
||||||
|
{typeof image === 'string' ? (
|
||||||
|
<Fragment set:html={image} />
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
class="mx-auto rounded-md w-full"
|
||||||
|
widths={[400, 768, 1024, 2040]}
|
||||||
|
sizes="(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px"
|
||||||
|
loading="eager"
|
||||||
|
width={1024}
|
||||||
|
height={576}
|
||||||
|
{...image}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
91
v3/src/components/widgets/Hero2.astro
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
---
|
||||||
|
import Image from '~/components/common/Image.astro';
|
||||||
|
import Button from '~/components/ui/Button.astro';
|
||||||
|
|
||||||
|
import type { Hero as Props } from '~/types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline,
|
||||||
|
|
||||||
|
content = await Astro.slots.render('content'),
|
||||||
|
actions = await Astro.slots.render('actions'),
|
||||||
|
image = await Astro.slots.render('image'),
|
||||||
|
|
||||||
|
id,
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="relative md:-mt-[76px] not-prose" {...id ? { id } : {}}>
|
||||||
|
<div class="absolute inset-0 pointer-events-none" aria-hidden="true">
|
||||||
|
<slot name="bg">
|
||||||
|
{bg ? <Fragment set:html={bg} /> : null}
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
<div class="relative max-w-7xl mx-auto px-4 sm:px-6">
|
||||||
|
<div class="pt-0 md:pt-[76px] pointer-events-none"></div>
|
||||||
|
<div class="py-12 md:py-20 lg:py-0 lg:flex lg:items-center lg:h-screen lg:gap-8">
|
||||||
|
<div class="basis-1/2 text-center lg:text-left pb-10 md:pb-16 mx-auto">
|
||||||
|
{
|
||||||
|
tagline && (
|
||||||
|
<p
|
||||||
|
class="text-base text-secondary dark:text-blue-200 font-bold tracking-wide uppercase"
|
||||||
|
set:html={tagline}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
title && (
|
||||||
|
<h1
|
||||||
|
class="text-5xl md:text-6xl font-bold leading-tighter tracking-tighter mb-4 font-heading dark:text-gray-200"
|
||||||
|
set:html={title}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<div class="max-w-3xl mx-auto lg:max-w-none">
|
||||||
|
{subtitle && <p class="text-xl text-muted mb-6 dark:text-slate-300" set:html={subtitle} />}
|
||||||
|
|
||||||
|
{
|
||||||
|
actions && (
|
||||||
|
<div class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4 lg:justify-start lg:m-0 lg:max-w-7xl">
|
||||||
|
{Array.isArray(actions) ? (
|
||||||
|
actions.map((action) => (
|
||||||
|
<div class="flex w-full sm:w-auto">
|
||||||
|
<Button {...(action || {})} class="w-full sm:mb-0" />
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<Fragment set:html={actions} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{content && <Fragment set:html={content} />}
|
||||||
|
</div>
|
||||||
|
<div class="basis-1/2">
|
||||||
|
{
|
||||||
|
image && (
|
||||||
|
<div class="relative m-auto max-w-5xl">
|
||||||
|
{typeof image === 'string' ? (
|
||||||
|
<Fragment set:html={image} />
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
class="mx-auto rounded-md w-full"
|
||||||
|
widths={[400, 768, 1024, 2040]}
|
||||||
|
sizes="(max-width: 767px) 400px, (max-width: 1023px) 768px, (max-width: 2039px) 1024px, 2040px"
|
||||||
|
loading="eager"
|
||||||
|
width={600}
|
||||||
|
height={600}
|
||||||
|
{...image}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
77
v3/src/components/widgets/HeroText.astro
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
---
|
||||||
|
import type { CallToAction } from '~/types';
|
||||||
|
import Button from '~/components/ui/Button.astro';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
tagline?: string;
|
||||||
|
content?: string;
|
||||||
|
callToAction?: string | CallToAction;
|
||||||
|
callToAction2?: string | CallToAction;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline,
|
||||||
|
content = await Astro.slots.render('content'),
|
||||||
|
callToAction = await Astro.slots.render('callToAction'),
|
||||||
|
callToAction2 = await Astro.slots.render('callToAction2'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="relative md:-mt-[76px] not-prose">
|
||||||
|
<div class="absolute inset-0 pointer-events-none" aria-hidden="true"></div>
|
||||||
|
<div class="relative max-w-7xl mx-auto px-4 sm:px-6">
|
||||||
|
<div class="pt-0 md:pt-[76px] pointer-events-none"></div>
|
||||||
|
<div class="py-12 md:py-20 pb-8 md:pb-8">
|
||||||
|
<div class="text-center max-w-5xl mx-auto">
|
||||||
|
{
|
||||||
|
tagline && (
|
||||||
|
<p
|
||||||
|
class="text-base text-secondary dark:text-blue-200 font-bold tracking-wide uppercase"
|
||||||
|
set:html={tagline}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
title && (
|
||||||
|
<h1
|
||||||
|
class="text-5xl md:text-6xl font-bold leading-tighter tracking-tighter mb-4 font-heading dark:text-gray-200"
|
||||||
|
set:html={title}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
<div class="max-w-3xl mx-auto">
|
||||||
|
{subtitle && <p class="text-xl text-muted mb-6 dark:text-slate-300" set:html={subtitle} />}
|
||||||
|
<div class="max-w-xs sm:max-w-md m-auto flex flex-nowrap flex-col sm:flex-row sm:justify-center gap-4">
|
||||||
|
{
|
||||||
|
callToAction && (
|
||||||
|
<div class="flex w-full sm:w-auto">
|
||||||
|
{typeof callToAction === 'string' ? (
|
||||||
|
<Fragment set:html={callToAction} />
|
||||||
|
) : (
|
||||||
|
<Button variant="primary" {...callToAction} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
{
|
||||||
|
callToAction2 && (
|
||||||
|
<div class="flex w-full sm:w-auto">
|
||||||
|
{typeof callToAction2 === 'string' ? (
|
||||||
|
<Fragment set:html={callToAction2} />
|
||||||
|
) : (
|
||||||
|
<Button {...callToAction2} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{content && <Fragment set:html={content} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
11
v3/src/components/widgets/Note.astro
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="bg-blue-50 dark:bg-slate-800 not-prose">
|
||||||
|
<div class="max-w-6xl mx-auto px-4 sm:px-6 py-4 text-md text-center font-medium">
|
||||||
|
<span class="font-bold">
|
||||||
|
<Icon name="tabler:info-square" class="w-5 h-5 inline-block align-text-bottom" /> Philosophy:</span
|
||||||
|
> Simplicity, Best Practices and High Performance
|
||||||
|
</div>
|
||||||
|
</section>
|
83
v3/src/components/widgets/Pricing.astro
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import Button from '~/components/ui/Button.astro';
|
||||||
|
import Headline from '~/components/ui/Headline.astro';
|
||||||
|
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||||
|
import type { Pricing as Props } from '~/types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = '',
|
||||||
|
subtitle = '',
|
||||||
|
tagline = '',
|
||||||
|
prices = [],
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-7xl mx-auto ${classes?.container ?? ''}`} bg={bg}>
|
||||||
|
<Headline title={title} subtitle={subtitle} tagline={tagline} />
|
||||||
|
<div class="flex items-stretch justify-center">
|
||||||
|
<div class="grid grid-cols-3 gap-4 dark:text-white sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-3">
|
||||||
|
{
|
||||||
|
prices &&
|
||||||
|
prices.map(({ title, subtitle, price, period, items, callToAction, hasRibbon = false, ribbonTitle }) => (
|
||||||
|
<div class="col-span-3 mx-auto flex w-full sm:col-span-1 md:col-span-1 lg:col-span-1 xl:col-span-1">
|
||||||
|
{price && period && (
|
||||||
|
<div class="rounded-lg backdrop-blur border border-gray-200 dark:border-gray-700 bg-white dark:bg-slate-900 shadow px-6 py-8 flex w-full max-w-sm flex-col justify-between text-center">
|
||||||
|
{hasRibbon && ribbonTitle && (
|
||||||
|
<div class="absolute right-[-5px] 2xl:right-[-8px] rtl:right-auto rtl:left-[-8px] rtl:2xl:left-[-10px] top-[-5px] 2xl:top-[-10px] z-[1] h-[100px] w-[100px] overflow-hidden text-right">
|
||||||
|
<span class="absolute top-[19px] right-[-21px] rtl:right-auto rtl:left-[-21px] block w-full rotate-45 rtl:-rotate-45 bg-green-700 text-center text-[10px] font-bold uppercase leading-5 text-white shadow-[0_3px_10px_-5px_rgba(0,0,0,0.3)] before:absolute before:left-0 before:top-full before:z-[-1] before:border-[3px] before:border-r-transparent before:border-b-transparent before:border-l-green-800 before:border-t-green-800 before:content-[''] after:absolute after:right-0 after:top-full after:z-[-1] after:border-[3px] after:border-l-transparent after:border-b-transparent after:border-r-green-800 after:border-t-green-800 after:content-['']">
|
||||||
|
{ribbonTitle}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div class="px-2 py-0">
|
||||||
|
{title && (
|
||||||
|
<h3 class="text-center text-xl font-semibold uppercase leading-6 tracking-wider mb-2">{title}</h3>
|
||||||
|
)}
|
||||||
|
{subtitle && <p class="font-light sm:text-lg text-gray-600 dark:text-slate-400">{subtitle}</p>}
|
||||||
|
<div class="my-8">
|
||||||
|
<div class="flex items-center justify-center text-center mb-1">
|
||||||
|
<span class="text-5xl">$</span>
|
||||||
|
<span class="text-6xl font-extrabold">{price}</span>
|
||||||
|
</div>
|
||||||
|
<span class="text-base leading-6 lowercase text-gray-600 dark:text-slate-400">{period}</span>
|
||||||
|
</div>
|
||||||
|
{items && (
|
||||||
|
<ul class="my-8 md:my-10 space-y-2 text-left">
|
||||||
|
{items.map(
|
||||||
|
({ description, icon }) =>
|
||||||
|
description && (
|
||||||
|
<li class="mb-1.5 flex items-start space-x-3 leading-7">
|
||||||
|
<div class="rounded-full bg-primary mt-1">
|
||||||
|
<Icon name={icon ? icon : 'tabler:check'} class="w-5 h-5 font-bold p-1 text-white" />
|
||||||
|
</div>
|
||||||
|
<span>{description}</span>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{callToAction && (
|
||||||
|
<div class={`flex justify-center`}>
|
||||||
|
{typeof callToAction === 'string' ? (
|
||||||
|
<Fragment set:html={callToAction} />
|
||||||
|
) : (
|
||||||
|
callToAction &&
|
||||||
|
callToAction.href && <Button {...(hasRibbon ? { variant: 'primary' } : {})} {...callToAction} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</WidgetWrapper>
|
46
v3/src/components/widgets/Stats.astro
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
import type { Stats as Props } from '~/types';
|
||||||
|
import WidgetWrapper from '../ui/WidgetWrapper.astro';
|
||||||
|
import Headline from '../ui/Headline.astro';
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline,
|
||||||
|
stats = [],
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-6xl mx-auto ${classes?.container ?? ''}`} bg={bg}>
|
||||||
|
<Headline title={title} subtitle={subtitle} tagline={tagline} />
|
||||||
|
<div class="flex flex-wrap justify-center -m-4 text-center">
|
||||||
|
{
|
||||||
|
stats &&
|
||||||
|
stats.map(({ amount, title, icon }) => (
|
||||||
|
<div class="p-4 md:w-1/4 sm:w-1/2 w-full min-w-[220px] text-center md:border-r md:last:border-none dark:md:border-slate-500">
|
||||||
|
{icon && (
|
||||||
|
<div class="flex items-center justify-center mx-auto mb-4 text-primary">
|
||||||
|
<Icon name={icon} class="w-10 h-10" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{amount && (
|
||||||
|
<div class="font-heading text-primary text-[2.6rem] font-bold dark:text-white lg:text-5xl xl:text-6xl">
|
||||||
|
{amount}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{title && (
|
||||||
|
<div class="text-sm font-medium uppercase tracking-widest text-gray-800 dark:text-slate-400 lg:text-base">
|
||||||
|
{title}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</WidgetWrapper>
|
59
v3/src/components/widgets/Steps.astro
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||||
|
import Timeline from '~/components/ui/Timeline.astro';
|
||||||
|
import Headline from '~/components/ui/Headline.astro';
|
||||||
|
import Image from '~/components/common/Image.astro';
|
||||||
|
import type { Steps as Props } from '~/types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline = await Astro.slots.render('tagline'),
|
||||||
|
items = [],
|
||||||
|
image = await Astro.slots.render('image'),
|
||||||
|
isReversed = false,
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-5xl ${classes?.container ?? ''}`} bg={bg}>
|
||||||
|
<div class:list={['flex flex-col gap-8 md:gap-12', { 'md:flex-row-reverse': isReversed }, { 'md:flex-row': image }]}>
|
||||||
|
<div class:list={['md:py-4 md:self-center', { 'md:basis-1/2': image }, { 'w-full': !image }]}>
|
||||||
|
<Headline
|
||||||
|
title={title}
|
||||||
|
subtitle={subtitle}
|
||||||
|
tagline={tagline}
|
||||||
|
classes={{
|
||||||
|
container: 'text-left rtl:text-right',
|
||||||
|
title: 'text-3xl lg:text-4xl',
|
||||||
|
...((classes?.headline as object) ?? {}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Timeline items={items} classes={classes?.items as Record<string, never>} />
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
image && (
|
||||||
|
<div class="relative md:basis-1/2">
|
||||||
|
{typeof image === 'string' ? (
|
||||||
|
<Fragment set:html={image} />
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
class="inset-0 object-cover object-top w-full rounded-md shadow-lg md:absolute md:h-full bg-gray-400 dark:bg-slate-700"
|
||||||
|
widths={[400, 768]}
|
||||||
|
sizes="(max-width: 768px) 100vw, 432px"
|
||||||
|
width={432}
|
||||||
|
height={768}
|
||||||
|
layout="cover"
|
||||||
|
src={image?.src}
|
||||||
|
alt={image?.alt || ''}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</WidgetWrapper>
|
72
v3/src/components/widgets/Steps2.astro
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||||
|
import Headline from '~/components/ui/Headline.astro';
|
||||||
|
import Button from '~/components/ui/Button.astro';
|
||||||
|
import type { Steps as Props } from '~/types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = await Astro.slots.render('title'),
|
||||||
|
subtitle = await Astro.slots.render('subtitle'),
|
||||||
|
tagline,
|
||||||
|
callToAction = await Astro.slots.render('callToAction'),
|
||||||
|
items = [],
|
||||||
|
isReversed = false,
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-6xl mx-auto ${classes?.container ?? ''}`} bg={bg}>
|
||||||
|
<div class={`flex flex-col gap-8 md:gap-12 md:flex-row ${isReversed ? 'md:flex-row-reverse' : ''}`}>
|
||||||
|
<div class={`w-full lg:w-1/2 gap-8 md:gap-12 ${isReversed ? 'lg:ml-16 md:ml-8 ml-0' : 'lg:mr-16 md:mr-8 mr-0'}`}>
|
||||||
|
<Headline
|
||||||
|
title={title}
|
||||||
|
subtitle={subtitle}
|
||||||
|
tagline={tagline}
|
||||||
|
classes={{
|
||||||
|
container: 'text-center md:text-left rtl:md:text-right mb-4 md:mb-8',
|
||||||
|
title: 'mb-4 text-3xl lg:text-4xl font-bold font-heading',
|
||||||
|
subtitle: 'mb-8 text-xl text-muted dark:text-slate-400',
|
||||||
|
// ...((classes?.headline as {}) ?? {}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="w-full text-center md:text-left rtl:md:text-right">
|
||||||
|
{
|
||||||
|
typeof callToAction === 'string' ? (
|
||||||
|
<Fragment set:html={callToAction} />
|
||||||
|
) : (
|
||||||
|
callToAction &&
|
||||||
|
callToAction.text &&
|
||||||
|
callToAction.href && <Button variant="primary" {...callToAction} class="mb-12 w-auto" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full lg:w-1/2 px-0">
|
||||||
|
<ul class="space-y-10">
|
||||||
|
{
|
||||||
|
items && items.length
|
||||||
|
? items.map(({ title: title2, description, icon }, index) => (
|
||||||
|
<li class="flex md:-mx-4">
|
||||||
|
<div class="pr-4 sm:pl-4 rtl:pr-0 rtl:pl-4 rtl:sm:pl-0 rtl:sm:pr-4">
|
||||||
|
<span class="flex w-16 h-16 mx-auto items-center justify-center text-2xl font-bold rounded-full bg-blue-100 text-primary">
|
||||||
|
{icon ? <Icon name={icon} class="w-6 h-6 icon-bold" /> : index + 1}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="pl-4 rtl:pl-0 rtl:pr-4">
|
||||||
|
<h3 class="mb-4 text-xl font-semibold font-heading" set:html={title2} />
|
||||||
|
<p class="text-muted dark:text-gray-400" set:html={description} />
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))
|
||||||
|
: ''
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</WidgetWrapper>
|
75
v3/src/components/widgets/Testimonials.astro
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
---
|
||||||
|
import Headline from '~/components/ui/Headline.astro';
|
||||||
|
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||||
|
import Button from '~/components/ui/Button.astro';
|
||||||
|
import Image from '~/components/common/Image.astro';
|
||||||
|
import type { Testimonials as Props } from '~/types';
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = '',
|
||||||
|
subtitle = '',
|
||||||
|
tagline = '',
|
||||||
|
testimonials = [],
|
||||||
|
callToAction,
|
||||||
|
|
||||||
|
id,
|
||||||
|
isDark = false,
|
||||||
|
classes = {},
|
||||||
|
bg = await Astro.slots.render('bg'),
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<WidgetWrapper id={id} isDark={isDark} containerClass={`max-w-6xl mx-auto ${classes?.container ?? ''}`} bg={bg}>
|
||||||
|
<Headline title={title} subtitle={subtitle} tagline={tagline} />
|
||||||
|
|
||||||
|
<div class="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{
|
||||||
|
testimonials &&
|
||||||
|
testimonials.map(({ title, testimonial, name, job, image }) => (
|
||||||
|
<div class="flex h-auto">
|
||||||
|
<div class="flex flex-col p-4 md:p-6 rounded-md shadow-xl dark:shadow-none dark:border dark:border-slate-600">
|
||||||
|
{title && <h2 class="text-lg font-medium leading-6 pb-4">{title}</h2>}
|
||||||
|
{testimonial && (
|
||||||
|
<blockquote class="flex-auto">
|
||||||
|
<p class="text-muted">" {testimonial} "</p>
|
||||||
|
</blockquote>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<hr class="border-slate-200 dark:border-slate-600 my-4" />
|
||||||
|
|
||||||
|
<div class="flex items-center">
|
||||||
|
{image && (
|
||||||
|
<div class="h-10 w-10 rounded-full border border-slate-200 dark:border-slate-600">
|
||||||
|
{typeof image === 'string' ? (
|
||||||
|
<Fragment set:html={image} />
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
class="h-10 w-10 rounded-full border border-slate-200 dark:border-slate-600 min-w-full min-h-full"
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
|
widths={[400, 768]}
|
||||||
|
layout="fixed"
|
||||||
|
{...image}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div class="grow ml-3 rtl:ml-0 rtl:mr-3">
|
||||||
|
{name && <p class="text-base font-semibold">{name}</p>}
|
||||||
|
{job && <p class="text-xs text-muted">{job}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
{
|
||||||
|
callToAction && (
|
||||||
|
<div class="flex justify-center mx-auto w-fit mt-8 md:mt-12 font-medium">
|
||||||
|
<Button {...callToAction} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</WidgetWrapper>
|
72
v3/src/config.yaml
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
site:
|
||||||
|
name: AstroWind
|
||||||
|
site: 'https://astrowind.vercel.app'
|
||||||
|
base: '/'
|
||||||
|
trailingSlash: false
|
||||||
|
|
||||||
|
googleSiteVerificationId: orcPxI47GSa-cRvY11tUe6iGg2IO_RPvnA1q95iEM3M
|
||||||
|
|
||||||
|
# Default SEO metadata
|
||||||
|
metadata:
|
||||||
|
title:
|
||||||
|
default: AstroWind
|
||||||
|
template: '%s — AstroWind'
|
||||||
|
description: "\U0001F680 Suitable for Startups, Small Business, Sass Websites, Professional Portfolios, Marketing Websites, Landing Pages & Blogs."
|
||||||
|
robots:
|
||||||
|
index: true
|
||||||
|
follow: true
|
||||||
|
openGraph:
|
||||||
|
site_name: AstroWind
|
||||||
|
images:
|
||||||
|
- url: '~/assets/images/default.png'
|
||||||
|
width: 1200
|
||||||
|
height: 628
|
||||||
|
type: website
|
||||||
|
twitter:
|
||||||
|
handle: '@onwidget'
|
||||||
|
site: '@onwidget'
|
||||||
|
cardType: summary_large_image
|
||||||
|
|
||||||
|
i18n:
|
||||||
|
language: en
|
||||||
|
textDirection: ltr
|
||||||
|
|
||||||
|
apps:
|
||||||
|
blog:
|
||||||
|
isEnabled: true
|
||||||
|
postsPerPage: 6
|
||||||
|
|
||||||
|
post:
|
||||||
|
isEnabled: true
|
||||||
|
permalink: '/%slug%' # Variables: %slug%, %year%, %month%, %day%, %hour%, %minute%, %second%, %category%
|
||||||
|
robots:
|
||||||
|
index: true
|
||||||
|
|
||||||
|
list:
|
||||||
|
isEnabled: true
|
||||||
|
pathname: 'blog' # Blog main path, you can change this to "articles" (/articles)
|
||||||
|
robots:
|
||||||
|
index: true
|
||||||
|
|
||||||
|
category:
|
||||||
|
isEnabled: true
|
||||||
|
pathname: 'category' # Category main path /category/some-category, you can change this to "group" (/group/some-category)
|
||||||
|
robots:
|
||||||
|
index: true
|
||||||
|
|
||||||
|
tag:
|
||||||
|
isEnabled: true
|
||||||
|
pathname: 'tag' # Tag main path /tag/some-tag, you can change this to "topics" (/topics/some-category)
|
||||||
|
robots:
|
||||||
|
index: false
|
||||||
|
|
||||||
|
isRelatedPostsEnabled: true
|
||||||
|
relatedPostsCount: 4
|
||||||
|
|
||||||
|
analytics:
|
||||||
|
vendors:
|
||||||
|
googleAnalytics:
|
||||||
|
id: null # or "G-XXXXXXXXXX"
|
||||||
|
|
||||||
|
ui:
|
||||||
|
theme: 'system' # Values: "system" | "light" | "dark" | "light:only" | "dark:only"
|
68
v3/src/content/config.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { z, defineCollection } from 'astro:content';
|
||||||
|
|
||||||
|
const metadataDefinition = () =>
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
title: z.string().optional(),
|
||||||
|
ignoreTitleTemplate: z.boolean().optional(),
|
||||||
|
|
||||||
|
canonical: z.string().url().optional(),
|
||||||
|
|
||||||
|
robots: z
|
||||||
|
.object({
|
||||||
|
index: z.boolean().optional(),
|
||||||
|
follow: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
|
||||||
|
description: z.string().optional(),
|
||||||
|
|
||||||
|
openGraph: z
|
||||||
|
.object({
|
||||||
|
url: z.string().optional(),
|
||||||
|
siteName: z.string().optional(),
|
||||||
|
images: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
url: z.string(),
|
||||||
|
width: z.number().optional(),
|
||||||
|
height: z.number().optional(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
locale: z.string().optional(),
|
||||||
|
type: z.string().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
|
||||||
|
twitter: z
|
||||||
|
.object({
|
||||||
|
handle: z.string().optional(),
|
||||||
|
site: z.string().optional(),
|
||||||
|
cardType: z.string().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
.optional();
|
||||||
|
|
||||||
|
const postCollection = defineCollection({
|
||||||
|
schema: z.object({
|
||||||
|
publishDate: z.date().optional(),
|
||||||
|
updateDate: z.date().optional(),
|
||||||
|
draft: z.boolean().optional(),
|
||||||
|
|
||||||
|
title: z.string(),
|
||||||
|
excerpt: z.string().optional(),
|
||||||
|
image: z.string().optional(),
|
||||||
|
|
||||||
|
category: z.string().optional(),
|
||||||
|
tags: z.array(z.string()).optional(),
|
||||||
|
author: z.string().optional(),
|
||||||
|
|
||||||
|
metadata: metadataDefinition(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collections = {
|
||||||
|
post: postCollection,
|
||||||
|
};
|
207
v3/src/content/post/astrowind-template-in-depth.mdx
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
---
|
||||||
|
publishDate: 2023-07-17T00:00:00Z
|
||||||
|
title: AstroWind template in depth
|
||||||
|
excerpt: While easy to get started, Astrowind is quite complex internally. This page provides documentation on some of the more intricate parts.
|
||||||
|
image: https://images.unsplash.com/photo-1534307671554-9a6d81f4d629?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1651&q=80
|
||||||
|
category: Documentation
|
||||||
|
tags:
|
||||||
|
- astro
|
||||||
|
- tailwind css
|
||||||
|
- front-end
|
||||||
|
metadata:
|
||||||
|
canonical: https://astrowind.vercel.app/astrowind-template-in-depth
|
||||||
|
---
|
||||||
|
|
||||||
|
import DListItem from '~/components/ui/DListItem.astro';
|
||||||
|
import ToggleTheme from '~/components/common/ToggleTheme.astro';
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
It can be a somewhat daunting task trying to get a handle on _AstroWind_ internals, and particularly various points of usage.
|
||||||
|
|
||||||
|
This page outlines and clarifies some of the techniques found in _AstroWind_. Use it as a guide for further modification, or an instructional for techniques to use in your own endeavors.
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
|
||||||
|
As the name suggests, _AstroWind_ relies on _TailWind_ for styling. Furthermore, _AstroWind_ defines custom low level style settings which are incorporated into _TailWind_ seamlessly, and which provides consistency for higher level styling constructs, as well as enabling dark mode.
|
||||||
|
|
||||||
|
The styling mechanism consists of the following files (all paths are prefixed with `/src/` ):
|
||||||
|
|
||||||
|
<DListItem dt="assets/styles/tailwind.css">
|
||||||
|
This file is essentially an extension of _TailWind's_ base.css. High-level component styles are defined here. Note
|
||||||
|
also styling on elements selected by 'attribute' selectors at the bottom of the files, particularly those selected by
|
||||||
|
'data' attributes.
|
||||||
|
</DListItem>
|
||||||
|
<DListItem dt="components/CustomStyles.astro">
|
||||||
|
Defines custom colors and fonts. For these to take effect in the 'base.css' file, they need to be loaded in the html
|
||||||
|
header section. See next.
|
||||||
|
</DListItem>
|
||||||
|
<DListItem dt="layouts/Layout.astro">
|
||||||
|
This layout is used for all of the pages rendered by _AstroWind_. The contents of _tailwind.css_ and
|
||||||
|
_CustomStyles.astro_ component, described above, is injected into the html header.
|
||||||
|
</DListItem>
|
||||||
|
|
||||||
|
### Dark Mode
|
||||||
|
|
||||||
|
_Dark Mode_ is triggered by the little 'sunlight' icon:<ToggleTheme/>in the page header. It is defined in the _components/common/ToggleTheme.astro_, but the event is attached and the action defined in _components/common/BasicScripts.astro_ in the following snippet:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
attachEvent('[data-aw-toggle-color-scheme]', 'click', function () {
|
||||||
|
if (defaultTheme.endsWith(':only')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
document.documentElement.classList.toggle('dark');
|
||||||
|
localStorage.theme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that this is a client event. _BasicScripts.astro_ defines several other client-side functionality as well as this one.
|
||||||
|
|
||||||
|
## Advanced Slot Usage
|
||||||
|
|
||||||
|
_slots_ are part of the component implementation, which is a common concept among many frameworks, including _Astrojs_. The typical slot definition in a component looks like this:
|
||||||
|
|
||||||
|
```astro
|
||||||
|
---
|
||||||
|
// (file: MyComponent.astro)
|
||||||
|
const { title } = Astro.props;
|
||||||
|
export interface Props {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2>{title}</h2>
|
||||||
|
<slot />
|
||||||
|
<!-- slot contents injected here -->
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
And in usage elsewhere:
|
||||||
|
|
||||||
|
```astro
|
||||||
|
import MyComponent from "~/components/MyComponent"; ...
|
||||||
|
<MyComponent someArg="A Slot example">
|
||||||
|
<p>This content will be displayed in the slot</p>
|
||||||
|
</MyComponent>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Alternate usage
|
||||||
|
|
||||||
|
There's another way we can use slots, useful particularly when a component can have markdown content is as follows (study carefully...):
|
||||||
|
|
||||||
|
```astro
|
||||||
|
---
|
||||||
|
// (file: MyComponent.astro)
|
||||||
|
|
||||||
|
const { title } = Astro.props;
|
||||||
|
export interface Props {
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
const content: string = await Astro.props.render('default');
|
||||||
|
---
|
||||||
|
|
||||||
|
// renders the html to the 'content' variable
|
||||||
|
<div>
|
||||||
|
<h2>{title}</h2>
|
||||||
|
<div set:html={content} />
|
||||||
|
<!-- slot contents injected here -->
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
Whoa!! What's going on here?
|
||||||
|
|
||||||
|
Notice there is no slot definition in the html portion of the component. Instead, what we do is have _Astro_ render the slot content (here, the 'default' content, but you can also render named slots) into a variable, and then use that content in a _div_ (for instance).
|
||||||
|
|
||||||
|
So, if the usage is in a markdown file, like so:
|
||||||
|
|
||||||
|
```mdx
|
||||||
|
import MyComponent from '../../components/MyComponent';
|
||||||
|
|
||||||
|
# Using the above component in a .mdx file (that can take components)
|
||||||
|
|
||||||
|
{' '}
|
||||||
|
|
||||||
|
<MyComponent title="This is a slot implementor">### Here is some markdown content - With a bullet item.</MyComponent>
|
||||||
|
```
|
||||||
|
|
||||||
|
_MyComponent_ renders the markdown to html and then injects it into the div.
|
||||||
|
|
||||||
|
This actually has a big advantage -- consider that with the normal usage you don't have access to the slot contents: _Astro_ just plops the content into the _<slot/>_ tag. Using this method, however, allows you to access the content and further manipulate it before it gets inserted into the html.
|
||||||
|
|
||||||
|
This allows a great deal of flexibility in component design.
|
||||||
|
|
||||||
|
### Yet Another Step
|
||||||
|
|
||||||
|
Now, we get to the techniques used in _AstroWind_, we'll use the _pages/index.astro_ file to illustrate.
|
||||||
|
|
||||||
|
You'll note that the index file imports a lot of components, each one roughly analagous to a panel in the index page. Each of these components, in turn, is instantiated sequentially throughout the page. But, you'll notice that some of them use this kind of construct (we'll use the last section, _CallToAction_, as it is most illustrative of the technique):
|
||||||
|
|
||||||
|
```astro
|
||||||
|
<CallToAction
|
||||||
|
callToAction={{
|
||||||
|
text: 'Get template',
|
||||||
|
href: 'https://github.com/onwidget/astrowind',
|
||||||
|
icon: 'tabler:download',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Fragment slot="title">
|
||||||
|
Astro + <br class="block sm:hidden" /><span class="sm:whitespace-nowrap">Tailwind CSS</span>
|
||||||
|
</Fragment>
|
||||||
|
|
||||||
|
<Fragment slot="subtitle">
|
||||||
|
Be very surprised by these huge fake numbers you are seeing on this page. <br class="hidden md:inline" />Don't waste
|
||||||
|
more time! :P
|
||||||
|
</Fragment>
|
||||||
|
</CallToAction>
|
||||||
|
```
|
||||||
|
|
||||||
|
Some things to note, here:
|
||||||
|
|
||||||
|
<DListItem dt="The <em>callToAction</em> argument">
|
||||||
|
This argument is actually being passed a javascript object -- not a string. (However, in the TS definition, it could
|
||||||
|
be a string...)
|
||||||
|
</DListItem>
|
||||||
|
<DListItem dt="There are several <em>Fragment</em> children">
|
||||||
|
Furthermore, these <Fragment/> elements each have a _slot="(value)"_ specifier.
|
||||||
|
</DListItem>
|
||||||
|
|
||||||
|
The latter seems odd, because <Fragment/> is a built-in component over which you have no control, and doesn't have a provision for rendering slots, <em>per se</em>.
|
||||||
|
|
||||||
|
The answer lies in a paragraph in the _Astro_ docs, slots section, which states:
|
||||||
|
|
||||||
|
> Use a `slot="my-slot"` attribute on the child element that you want to pass through to a matching slot `name="my-slot" />` placeholder in your component.
|
||||||
|
|
||||||
|
That's pretty concise and a bit of a head-scratcher to read, but basically what it says is that:
|
||||||
|
|
||||||
|
1. Given a component that defines a slot:
|
||||||
|
1. you can reference a slot from a child element of that component and,
|
||||||
|
1. provide content to the parent component's slot from the child by naming the slot in the child with a `slot="<slot-name>"` property assignment, where the _slot-name_ is the parent's slot.
|
||||||
|
|
||||||
|
So, in the example above, the _CallToAction_ component defines the _subtitle_ slot, and the following _<Fragment slot="subtitle">_ populates the slot with the following content:
|
||||||
|
|
||||||
|
```astro
|
||||||
|
<Fragment slot="subtitle">
|
||||||
|
Be very surprised by these huge fake numbers you are seeing on this page. <br class="hidden md:inline" />Don't waste
|
||||||
|
more time! :P
|
||||||
|
</Fragment>
|
||||||
|
```
|
||||||
|
|
||||||
|
And, the _CallToAction_ component defines and renders it thusly:
|
||||||
|
|
||||||
|
```astro
|
||||||
|
---
|
||||||
|
//...
|
||||||
|
const { subtitle = await Astro.slots.render('subtitle') } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
//...
|
||||||
|
{subtitle && <p class="text-xl text-muted dark:text-slate-400" set:html={subtitle} />}
|
||||||
|
//...
|
||||||
|
```
|
||||||
|
|
||||||
|
There's a lot to wrap your head around, here.
|
||||||
|
|
||||||
|
Notice first that _subtitle_ is defined as a prop/argument, but it's being processed as a slot. Interestingly, prop args and slots seem to be somewhat interchangeable: if the subtitle was just a string, it would simply take that assignment. The main difference is that if you render them independently, you have to call the render with an _await_ modifier.
|
@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
publishDate: 2023-08-12T00:00:00Z
|
||||||
|
author: John Smith
|
||||||
|
title: Get started with AstroWind to create a website using Astro and Tailwind CSS
|
||||||
|
excerpt: Start your web journey with AstroWind – harness Astro and Tailwind CSS for a stunning site. Explore our guide now.
|
||||||
|
image: https://images.unsplash.com/photo-1516996087931-5ae405802f9f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80
|
||||||
|
category: Tutorials
|
||||||
|
tags:
|
||||||
|
- astro
|
||||||
|
- tailwind css
|
||||||
|
metadata:
|
||||||
|
canonical: https://astrowind.vercel.app/get-started-website-with-astro-tailwind-css
|
||||||
|
---
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||||
|
|
||||||
|
## Nostra torquent consequat volutpat aliquet neque
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet consectetur adipiscing elit proin, aenean litora volutpat urna egestas magnis arcu non, cras ut cursus et sed morbi lectus. Integer faucibus sagittis eu nunc urna aliquet a laoreet torquent, suspendisse penatibus nulla sollicitudin congue rutrum dictum. Ornare mi habitasse fermentum phasellus dui et morbi litora sodales dictum id erat, nibh purus class ligula aenean lectus venenatis euismod cras torquent ac. Senectus sagittis conubia hendrerit at egestas porta venenatis nisi metus gravida tempor, aenean facilisis nisl ante facilisi lacus integer hac iaculis purus. Scelerisque libero torquent egestas curae tellus viverra inceptos imperdiet urna, porta suspendisse interdum primis odio morbi tempor commodo dictumst, suscipit ornare habitasse semper feugiat cras quisque lobortis.
|
||||||
|
|
||||||
|
Iaculis arcu commodo dis proin vitae himenaeos, ante tristique potenti magna ligula, sagittis libero fermentum ullamcorper sociis. Sem eros non arcu natoque fringilla lacus vestibulum lacinia integer mus viverra in proin, sagittis fusce tortor erat enim rutrum vulputate curae laoreet class diam. Inceptos convallis ac nisi natoque nam quisque magnis ut nullam fringilla curae, luctus lacus purus habitant erat magna molestie class habitasse metus, nibh lobortis tortor curabitur neque phasellus feugiat netus morbi parturient. Neque malesuada mauris justo himenaeos pharetra, ullamcorper enim ligula a nulla consequat, eget vivamus velit ridiculus.
|
||||||
|
|
||||||
|
## Praesent tellus ad sapien erat or
|
||||||
|
|
||||||
|
- Quam orci nostra mi nulla, hac a.
|
||||||
|
|
||||||
|
- Interdum iaculis quis tellus sociis orci nulla, quam rutrum conubia tortor primis.
|
||||||
|
|
||||||
|
- Non felis sem placerat aenean duis, ornare turpis nostra.
|
||||||
|
|
||||||
|
- Habitasse duis sociis sagittis cursus, ante dictumst commodo.
|
||||||
|
|
||||||
|
Duis maecenas massa habitasse inceptos imperdiet scelerisque at condimentum ultrices, nam dui leo enim taciti varius cras habitant pretium rhoncus, ut hac euismod nostra metus sagittis mi aenean. Quam eleifend aliquet litora eget a tempor, ultricies integer vestibulum non felis sodales, eros diam massa libero iaculis.
|
||||||
|
|
||||||
|
Nisl ligula ante magnis himenaeos pellentesque orci cras integer urna ut convallis, id phasellus libero est nunc ultrices eget blandit massa ac hac, morbi vulputate quisque tellus feugiat conubia luctus tincidunt curae fermentum. Venenatis dictumst tincidunt senectus vivamus duis dis sociis taciti porta primis, rhoncus ridiculus rutrum curae mattis ullamcorper ac sagittis nascetur curabitur erat, faucibus placerat vulputate eu at habitasse nulla nisl interdum. Varius turpis dignissim montes ac ante tristique quis parturient hendrerit faucibus, consequat auctor penatibus suspendisse rutrum erat nulla inceptos est justo, etiam mollis mauris facilisi cras sociosqu eu sapien sed.
|
||||||
|
|
||||||
|
Blandit aptent conubia mollis mauris habitasse suspendisse torquent aenean, ac primis auctor congue cursus mi posuere molestie, velit elementum per feugiat libero dictumst phasellus. Convallis mollis taciti condimentum praesent id porttitor ac dictumst at, sed in eu eleifend vehicula fermentum lectus litora venenatis, gravida hac molestie cum sociosqu mus viverra torquent. Congue est fusce habitasse ridiculus integer suscipit platea volutpat, inceptos varius elementum pellentesque malesuada interdum magnis. Hac lacus eget enim purus massa commodo nec lectus natoque fames arcu, mattis class quam ut neque dui cras quis diam orci sed velit, erat morbi eros suscipit sagittis laoreet vivamus torquent nulla turpis.
|
||||||
|
|
||||||
|
Ridiculus velit suscipit consequat auctor interdum magna gravida dictumst libero ut habitasse, sollicitudin vehicula suspendisse leo erat tristique at platea sagittis proin dignissim, id ornare scelerisque et urna maecenas congue tincidunt dictum malesuada. Dui vulputate accumsan scelerisque ridiculus dictum quisque et nam hac, tempus ultricies curabitur proin netus diam vivamus. Vestibulum ante ac auctor mi urna risus lacinia vulputate justo orci sociis dui semper, commodo morbi enim vivamus neque sem pellentesque velit donec hac metus odio. Tempor ultrices himenaeos massa sollicitudin mus conubia scelerisque cubilia, nascetur potenti mauris convallis et lectus gravida egestas sociis, erat eros ultricies aptent congue tortor ornare.
|
||||||
|
|
||||||
|
Pretium aliquet sodales aliquam tincidunt litora lectus, erat dui nibh diam mus, sed hendrerit condimentum senectus arcu. Arcu a nibh auctor dapibus eros turpis tempus commodo, libero hendrerit dictum interdum mus class sed scelerisque, sapien dictumst enim magna molestie habitant donec. Fringilla dui sed curabitur commodo varius est vel, viverra primis habitant sapien montes mattis dignissim, gravida cubilia laoreet tempus aliquet senectus. Sociosqu purus praesent porttitor curae sollicitudin accumsan feugiat maecenas donec quis lacus, suscipit taciti convallis odio morbi eros nibh bibendum nunc orci. Magna cras nullam aliquam metus nibh sagittis facilisi tortor nec, mus varius curae ridiculus fames congue interdum erat urna, neque odio lobortis mi mattis diam cubilia arcu.
|
||||||
|
|
||||||
|
Laoreet fusce nec class porttitor mus proin aenean, velit vestibulum feugiat porta egestas sapien posuere, conubia nisi tempus varius hendrerit tortor. Congue aliquam scelerisque neque vivamus habitasse semper mauris pellentesque accumsan posuere, suspendisse lectus gravida erat sagittis arcu praesent mus ornare. Habitasse nibh nam morbi mollis senectus erat risus, cum sollicitudin class platea congue mattis venenatis, luctus aenean parturient hendrerit malesuada ante. Mus auctor tincidunt consequat massa tortor nulla luctus habitasse vestibulum quis velit, laoreet sagittis cum facilisi in sem tellus leo vulputate vehicula bibendum orci, felis nisl blandit lacus convallis congue turpis magna facilisis condimentum.
|
||||||
|
|
||||||
|
Dictumst pellentesque urna donec sociis suscipit montes consequat, commodo quam habitasse senectus fringilla maecenas, inceptos magna tristique eu nullam nam. Maecenas orci nibh hac eu tristique ut penatibus ultrices ante, pellentesque cubilia pharetra dis facilisis aliquam praesent malesuada vivamus, commodo cras velit convallis molestie nec tellus augue. Etiam ut convallis risus id dapibus platea laoreet accumsan, habitant et aenean netus inceptos iaculis per, mauris curae at ligula odio ad eu. Mauris erat tempor interdum sapien commodo per nullam tortor, fusce facilisis vehicula egestas dui nulla conubia ut fames, fringilla et tincidunt penatibus facilisi at mollis.
|
||||||
|
|
||||||
|
Fermentum sociosqu litora primis sollicitudin fusce diam consequat vehicula per lobortis et, viverra sodales magna rutrum sed mollis faucibus molestie purus montes est, risus nostra congue venenatis lectus enim torquent eros dis dapibus. Dui suscipit scelerisque massa ligula euismod accumsan augue, magna vel lacus ante nullam senectus commodo, viverra cubilia eros eget penatibus tempor. Mattis mauris hac felis semper dui sociis faucibus mollis ornare pretium aliquam velit nisl, quis litora sem at vel duis rutrum imperdiet natoque viverra himenaeos tempor.
|
||||||
|
|
||||||
|
Integer eu tristique purus luctus vivamus porttitor vel nisl, tortor malesuada augue vulputate diam velit pellentesque sodales, duis phasellus vestibulum fermentum leo facilisi porta. Hac porttitor cum dapibus volutpat quisque odio taciti nulla senectus mollis curae, accumsan suscipit cubilia tempor ligula in venenatis justo leo erat, magna tincidunt nullam lacinia luctus malesuada non vivamus praesent pharetra. Non quam felis montes pretium volutpat suspendisse lacus, torquent magna dictumst orci libero porta, feugiat taciti cras ridiculus aenean rutrum. Tellus nostra tincidunt hac in ligula mi vulputate venenatis pellentesque urna dui, at luctus tristique quisque vel a dignissim scelerisque platea pretium, suspendisse ante phasellus porttitor quis aliquam malesuada etiam enim nullam.
|
||||||
|
|
||||||
|
Hendrerit taciti litora nec facilisis diam vehicula magnis potenti, parturient velit egestas nisl lobortis tincidunt rutrum cursus, fusce senectus mi massa primis mattis rhoncus. Accumsan est ac varius consequat vulputate, ligula cursus euismod sagittis inceptos scelerisque, lacus malesuada torquent dictumst. Volutpat morbi metus urna rhoncus nunc tempor molestie, congue curabitur quis interdum posuere. Mollis viverra velit tortor mus netus nunc molestie metus, sem massa himenaeos luctus feugiat taciti iaculis fames porttitor, leo arcu consequat gravida dapibus pulvinar elementum.
|
@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
publishDate: 2023-08-06T00:00:00Z
|
||||||
|
title: How to customize AstroWind template to suit your branding
|
||||||
|
excerpt: Personalize AstroWind template for your brand. Our guide unlocks seamless customization steps for a unique online presence.
|
||||||
|
image: https://images.unsplash.com/photo-1546984575-757f4f7c13cf?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80
|
||||||
|
tags:
|
||||||
|
- astro
|
||||||
|
- tailwind css
|
||||||
|
- theme
|
||||||
|
metadata:
|
||||||
|
canonical: https://astrowind.vercel.app/how-to-customize-astrowind-to-your-brand
|
||||||
|
---
|
||||||
|
|
||||||
|
## Congue justo vulputate nascetur convallis varius orci fringilla nulla pharetr
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet consectetur adipiscing elit, augue malesuada natoque in ad erat aliquam facilisi, lacus rhoncus mattis nostra et a. Mauris malesuada rutrum dis libero egestas mus vulputate, fermentum ad morbi phasellus faucibus tellus leo urna, blandit ullamcorper diam imperdiet dictumst litora. Fringilla eros malesuada lobortis mi odio metus leo, blandit imperdiet augue fames aliquam ultricies tortor massa, duis magnis hendrerit id magna sociosqu. Aptent mi imperdiet id sapien suscipit ut netus turpis, lacinia ac porttitor potenti dui taciti at egestas, fermentum neque nascetur sodales tortor nunc congue.
|
||||||
|
|
||||||
|
Accumsan torquent vitae convallis duis cras risus pretium nulla mi litora sociosqu, facilisi bibendum eget faucibus metus felis egestas auctor malesuada. Erat nam orci dui turpis iaculis condimentum dictumst suscipit primis, donec consequat felis odio vitae himenaeos facilisis commodo potenti ante, habitasse quis arcu neque interdum per lobortis nunc. Ultricies lobortis ullamcorper sagittis et sollicitudin sociis sed dignissim posuere, nisi pharetra erat varius id aenean lacinia commodo morbi primis, ornare diam proin nunc volutpat nec dui egestas.
|
||||||
|
|
||||||
|
## Mauris velit laoreet vitae cursus augue
|
||||||
|
|
||||||
|
- Massa egestas consequat nisl id volutpat, varius neque aenean.
|
||||||
|
|
||||||
|
- Venenatis tincidunt eros pretium viverra lacinia convallis, turpis orci condimentum fusce.
|
||||||
|
|
||||||
|
- Pellentesque in aliquet nisi gravida netus, commodo aptent volutpat.
|
||||||
|
|
||||||
|
- Nisi rutrum eros euismod, parturient ullamcorper mattis a, dapibus vestibulum.
|
||||||
|
|
||||||
|
Senectus fermentum tristique egestas bibendum per dictumst purus pharetra cras dictum pulvinar, vitae nec eros montes dis quis nullam duis netus litora, feugiat cubilia mollis porttitor velit ligula metus ante risus eu.
|
||||||
|
|
||||||
|
Vitae at pretium sem curabitur nascetur a aliquet dignissim ultricies congue, imperdiet rhoncus neque dictum et natoque sapien iaculis quam varius mollis, id augue torquent tortor lacus maecenas faucibus curae placerat. Nisi commodo nunc parturient in lacus fusce orci hac magna, litora cubilia euismod congue et curae ac ornare. Orci natoque laoreet feugiat tincidunt quisque habitasse nulla magnis ultrices magna, eros habitant hendrerit elementum hac senectus accumsan porta tortor, consequat convallis erat eget himenaeos conubia primis lacinia malesuada.
|
||||||
|
|
||||||
|
Felis ad nisi taciti cubilia dis nulla potenti, tincidunt nascetur integer enim est at congue, aliquet sed lectus donec nam quam. Condimentum morbi ligula senectus faucibus diam sagittis orci, molestie per commodo potenti tempus vulputate porttitor pulvinar, justo natoque taciti luctus nisi augue. Ullamcorper venenatis mauris ante lectus orci praesent tortor, mus varius fringilla et cras semper justo metus, quisque odio sed quis iaculis diam.
|
||||||
|
|
||||||
|
Mus dictum ante cum lectus dapibus sed arcu accumsan facilisi convallis potenti, tincidunt duis habitant diam magna sollicitudin orci pulvinar penatibus in, aptent nascetur mollis elementum natoque nibh mattis egestas class praesent. Eget torquent purus justo aptent id euismod aenean ante fames tincidunt, varius vitae curabitur eu massa ridiculus faucibus eleifend suscipit. Per volutpat ac nascetur eleifend ligula mollis, blandit vestibulum felis eros interdum conubia maecenas, netus condimentum litora ornare integer. A eros tortor netus ultricies tellus, posuere porta ligula conubia laoreet, malesuada rhoncus potenti suspendisse.
|
||||||
|
|
||||||
|
Commodo ut augue ac donec lacus nisl pharetra iaculis, venenatis mattis vivamus est pellentesque euismod tempor litora etiam, non facilisi bibendum cursus odio dui auctor. Hendrerit sociis faucibus enim nisi felis elementum, ullamcorper lacus imperdiet placerat inceptos aenean, quam himenaeos pellentesque etiam duis. Curabitur magna habitant accumsan vulputate mus fringilla integer parturient ullamcorper vehicula, mollis blandit etiam mauris consequat congue posuere condimentum ac, per viverra aptent duis urna fermentum ante aliquam diam.
|
||||||
|
|
||||||
|
Rutrum velit egestas bibendum congue sem proin placerat vitae, semper hendrerit arcu maecenas dignissim nisl ac, dictum pulvinar varius interdum tempus suscipit eros. Ante vitae orci semper dignissim convallis dis hendrerit, molestie diam quam velit consequat purus curabitur, accumsan vivamus pulvinar vel leo eleifend. Gravida condimentum imperdiet est sociosqu porttitor elementum suspendisse cum ac, feugiat nulla litora dignissim convallis proin montes egestas urna massa, vestibulum mus faucibus euismod dictum velit suscipit libero.
|
||||||
|
|
||||||
|
Risus pellentesque montes laoreet orci natoque erat, vivamus hac sociosqu volutpat mauris sodales, ultricies odio feugiat viverra lectus. Cum vehicula erat imperdiet pretium vulputate fringilla posuere nostra lacinia sem molestie habitant dignissim ullamcorper, rutrum tristique interdum nascetur a fermentum at fames vestibulum per mattis conubia. Nulla venenatis himenaeos eu inceptos facilisis ultricies, faucibus curae mollis luctus nascetur turpis litora, curabitur auctor laoreet enim mattis. Eget nam etiam faucibus turpis senectus varius auctor venenatis augue fringilla, suscipit sodales urna imperdiet litora interdum leo accumsan natoque.
|
||||||
|
|
||||||
|
Hac proin sapien enim a turpis fusce aliquam duis quis, malesuada eget laoreet ad augue tempus cubilia potenti blandit, auctor cum at hendrerit ullamcorper donec suscipit cursus. Ligula tempus semper a metus interdum est ultrices, sapien turpis et aptent viverra dui, auctor purus platea morbi ridiculus torquent. Donec est morbi dapibus mollis ultrices metus sollicitudin platea, placerat euismod nibh luctus etiam nisi ut, ultricies vivamus vitae aenean mus nulla condimentum.
|
||||||
|
|
||||||
|
Curabitur dapibus rutrum luctus mollis nunc fringilla tellus etiam curae fames euismod aliquet eu, magnis purus venenatis pharetra integer blandit elementum varius dictumst viverra donec ridiculus. Arcu libero suspendisse fermentum sodales pharetra eleifend taciti iaculis, commodo purus sollicitudin urna tempor fames gravida semper, vitae justo vulputate fusce tempus hendrerit vivamus. Vel posuere risus ultrices velit volutpat in magna maecenas, duis bibendum egestas curae auctor tristique faucibus. Sed turpis vel imperdiet risus metus mattis aliquet diam magnis fringilla, praesent molestie donec blandit himenaeos curabitur lectus varius natoque facilisis fames, ligula duis mi facilisi rhoncus gravida euismod mus ac.
|
||||||
|
|
||||||
|
Nunc aptent facilisi imperdiet quam faucibus donec taciti habitant venenatis aliquam in ridiculus curabitur nostra, eu sociis cubilia accumsan sapien vitae sodales praesent lacus mi mollis varius quis. Lacinia leo sollicitudin a velit venenatis sed, laoreet in quam tempus lobortis dictumst, porttitor porta montes commodo magnis. Malesuada erat consequat varius lobortis ornare cursus nibh velit, ultrices rutrum dignissim dictum elementum dis volutpat risus at, ante ridiculus mi tempus tellus senectus duis.
|
||||||
|
|
||||||
|
Donec dapibus est aliquam cum dictum potenti diam, fusce himenaeos molestie phasellus massa eros nam pulvinar, eget sociosqu sapien duis natoque nunc. Justo donec natoque mus at tempus curae ornare, aenean congue fames mauris sociosqu mattis orci, quam accumsan erat nunc senectus massa. Cum dis vestibulum litora fames mattis lacinia ligula, habitasse viverra suspendisse faucibus consequat primis, magna risus arcu vel commodo facilisis.
|
||||||
|
|
||||||
|
Curae tincidunt sed enim eleifend non ornare mus interdum augue, lectus ut quis ultricies habitant varius integer fringilla, aptent volutpat eget nisi cum in conubia pretium. Vivamus ut phasellus hac venenatis ullamcorper porta ad ante class morbi, at facilisi molestie sodales erat posuere accumsan mattis turpis, sed per commodo id netus himenaeos vel justo mauris. Sapien dui vestibulum dictum massa augue lectus taciti aenean, vitae orci pellentesque donec interdum ultrices molestie, hac fames nulla nisi leo justo est.
|
||||||
|
|
||||||
|
Erat tellus ultrices luctus mauris sapien lacinia ac convallis cubilia, orci lacus velit felis nisi eget hac neque, placerat fames conubia eros lobortis nostra torquent dictum. Ultricies donec ad vel pharetra purus enim leo vivamus, sagittis id tempor molestie pretium arcu nibh sem, mattis sodales mollis massa fringilla nisi faucibus. Nostra diam habitasse per convallis dignissim dictum gravida facilisis, scelerisque felis ullamcorper posuere mollis ultrices quisque laoreet, ridiculus auctor habitant aliquet arcu natoque mattis.
|
||||||
|
|
||||||
|
Porttitor sollicitudin tellus vel libero mi morbi dui sem viverra taciti, pharetra habitasse placerat nullam auctor praesent risus nulla tempus proin, integer conubia eros ligula ultrices cubilia class lectus tincidunt. Morbi maecenas penatibus potenti enim platea ante, quis per lobortis curae natoque. Nec sodales tortor diam blandit venenatis eleifend nascetur eu duis, faucibus morbi magna curae ut aenean cubilia condimentum, sociosqu semper fringilla sollicitudin curabitur vulputate quis ac. Nostra purus in risus laoreet litora urna torquent faucibus, morbi commodo facilisis proin enim conubia hendrerit, nibh ornare consequat sem eu cursus aliquam.
|
||||||
|
|
||||||
|
Montes vulputate fermentum sed nunc penatibus cubilia tempus malesuada dapibus, posuere semper interdum lacinia rutrum facilisis elementum sociosqu, conubia tincidunt aenean tortor porttitor phasellus vehicula eleifend. Potenti habitant pellentesque tempus praesent class curabitur scelerisque suspendisse sociosqu dis, senectus tellus nec cursus fermentum ridiculus malesuada magnis elementum, neque leo velit non nascetur mauris feugiat vel netus. Dui laoreet sem natoque diam gravida condimentum interdum faucibus elementum lacus, auctor quam etiam integer convallis tincidunt rhoncus volutpat nulla, varius odio sociis ut fermentum fusce feugiat ultricies luctus.
|
||||||
|
|
||||||
|
Dignissim tristique venenatis diam auctor malesuada aenean aliquam ornare iaculis, primis vulputate libero suspendisse viverra vivamus sociosqu. Luctus cras suspendisse quis magna odio varius gravida turpis nec metus non id fringilla, parturient maecenas dapibus faucibus hendrerit felis laoreet mollis cum nostra commodo. Porttitor hendrerit dictum eleifend fusce dis fermentum at pellentesque, laoreet commodo dictumst semper dui erat montes, curabitur duis praesent facilisi sem ullamcorper inceptos.
|
||||||
|
|
||||||
|
Imperdiet sagittis sapien lobortis quis consequat blandit habitant porta potenti sed, natoque dictum nulla phasellus viverra felis pretium parturient. Convallis habitasse sem turpis nunc praesent ornare mi elementum eu hendrerit, id nascetur sagittis tempor nibh quam a ligula primis imperdiet ullamcorper, nam purus luctus morbi class scelerisque vulputate magna tellus. Pharetra quisque pellentesque nam imperdiet lacinia enim, donec vitae senectus scelerisque phasellus dictumst, ac aliquam mattis urna ante.
|
||||||
|
|
||||||
|
Habitant praesent pulvinar scelerisque per phasellus lobortis velit, magnis odio himenaeos primis curabitur senectus, nascetur ullamcorper convallis nunc placerat nisl. Porta tellus commodo praesent ullamcorper cursus senectus tempor vivamus, penatibus eu purus ultrices posuere mi sodales, urna quisque accumsan imperdiet convallis aptent nisl. Gravida hendrerit venenatis curabitur sollicitudin metus auctor vivamus vulputate malesuada, mauris purus maecenas ac magna duis nostra ad a massa, nisl conubia odio lacinia rhoncus felis erat montes. Nostra eros proin mi venenatis enim semper ad magnis netus, in vestibulum ornare ac fusce aliquet aptent non condimentum faucibus, tempor arcu potenti blandit magna consequat luctus nam.
|
152
v3/src/content/post/landing.md
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
---
|
||||||
|
publishDate: 2023-07-15T00:00:00Z
|
||||||
|
title: 'Mastering Landing Pages: Practical Guide for 2023'
|
||||||
|
excerpt: Ever clicked on an ad and found yourself on a page that seemed to really want you to do something? Congratulations, you've landed on a Landing Page!
|
||||||
|
image: https://images.unsplash.com/photo-1561069934-eee225952461?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80
|
||||||
|
tags:
|
||||||
|
- landing-pages
|
||||||
|
- front-end
|
||||||
|
- resources
|
||||||
|
metadata:
|
||||||
|
canonical: https://astrowind.vercel.app/landing
|
||||||
|
---
|
||||||
|
|
||||||
|
In the vast digital landscape, standing out is more than a desire—it's a necessity. Enter the world of Landing Pages, the unsung heroes of digital marketing. With the power of the AstroWind template, developed using Astro and Tailwind CSS, crafting these pages becomes even more intuitive. Let's dive deep into understanding, creating, and optimizing them.
|
||||||
|
|
||||||
|
## Landing Pages Unveiled
|
||||||
|
|
||||||
|
A **Landing Page** is a standalone web page, distinct from your main website. Crafted with a singular objective: to convert visitors into actionable leads or sales. It's where a visitor "lands" post-clicking on a marketing link or ad.
|
||||||
|
|
||||||
|
Imagine clicking on an ad for a limited-time discount on a popular shoe brand. This action guides you to a page that showcases the discounted shoes, featuring a clear "Buy Now" button. That's a Landing Page in action, focusing your attention solely on the offer.
|
||||||
|
|
||||||
|
## The Power of Precision
|
||||||
|
|
||||||
|
Unlike a homepage brimming with diverse content, a Landing Page is laser-focused. It eliminates potential distractions like excessive navigation, ensuring the visitor's attention remains undivided. The result? Higher conversion rates and a more streamlined user experience.
|
||||||
|
|
||||||
|
![Target](https://images.unsplash.com/photo-1596008194705-2091cd6764d4?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1639&q=80)
|
||||||
|
|
||||||
|
Think of a Landing Page as a digital salesperson. Just as a salesperson would pitch a product without distractions, a Landing Page promotes an offer without unnecessary links or information. It's like walking into a store with a single product on display, making your choice straightforward.
|
||||||
|
|
||||||
|
## Why Landing Pages Matter
|
||||||
|
|
||||||
|
In today's digital rush, hoping customers stumble upon you is a strategy of the past. Landing Pages are the future. They:
|
||||||
|
|
||||||
|
- **Guide Traffic**: Directing visitors seamlessly through the sales funnel.
|
||||||
|
- **Boost ROI**: Maximizing returns on marketing investments.
|
||||||
|
- **Personalize User Experience**: Tailoring content to specific audience segments.
|
||||||
|
|
||||||
|
For instance, if you're launching a new fitness app. A well-crafted Landing Page can target individuals interested in health and wellness, offering them a free trial. This targeted approach ensures that those genuinely interested in fitness are the ones you're engaging with.
|
||||||
|
|
||||||
|
## Crafting the Perfect Landing Page
|
||||||
|
|
||||||
|
Every element of your Landing Page should resonate with its core objective. Here's what a high-converting Landing Page entails:
|
||||||
|
|
||||||
|
- **Benefit-Centric Headline**: Your headline should instantly convey the value proposition.
|
||||||
|
- **Engaging Imagery**: Visuals that complement and enhance the content.
|
||||||
|
- **Compelling Copy**: Clear, concise, and persuasive text that speaks directly to the visitor's needs.
|
||||||
|
- **Clear Call-to-Action (CTA)**: A standout button or link urging the visitor to take action.
|
||||||
|
- **Minimalist Design**: A clutter-free layout that emphasizes the offer. For example, using a Tailwind CSS web template like AstroWind.
|
||||||
|
- **Trust Indicators**: Endorsements, reviews, and badges that bolster credibility.
|
||||||
|
|
||||||
|
Imagine browsing online for a writing course. You land on a page with a captivating headline: "Unlock the Writer Within." Below, there's an engaging image of a person writing, followed by persuasive text and a bright "Enroll Now" button. This Landing Page has effectively used its elements to entice you to sign up.
|
||||||
|
|
||||||
|
## Homepage vs. Landing Page
|
||||||
|
|
||||||
|
While both are pivotal, they serve distinct roles:
|
||||||
|
|
||||||
|
- **Homepage**: Offers a panoramic view of your brand, catering to diverse visitor intents.
|
||||||
|
- **Landing Page**: Zeros in on a single, specific action, be it signing up, purchasing, or downloading.
|
||||||
|
|
||||||
|
Consider a popular online store. Their homepage might display various product categories, from electronics to clothing. However, if they’re promoting a summer sale, the Landing Page would focus solely on summer products. This focused approach urges visitors to take action, encouraging them to "Shop the Summer Sale Now!"
|
||||||
|
|
||||||
|
## The Art of Optimization
|
||||||
|
|
||||||
|
The digital realm is ever-evolving. Regular tweaks based on analytics can ensure your Landing Page remains a conversion powerhouse. Embrace A/B testing to compare different versions and refine for optimal results.
|
||||||
|
|
||||||
|
Let's say you have a Landing Page for a new skincare product. Version A uses an image of the product, while Version B showcases a video review. A/B testing might reveal that Version B, with the video, has a 20% higher conversion rate. Such insights can be invaluable for future campaigns.
|
||||||
|
|
||||||
|
## Landing Pages in Action
|
||||||
|
|
||||||
|
Landing Pages are versatile tools in your marketing toolkit. They play a role in various scenarios: promoting a product launch, capturing emails for a newsletter, or driving event registrations. They’re not just about capturing leads but nurturing and converting them.
|
||||||
|
|
||||||
|
Presented below are several prevalent types of Landing Pages. Each link offers a prime example of its respective type. Additionally, we carefully craft each link in the form of a comprehensive guide.
|
||||||
|
|
||||||
|
This approach ensures that you observe the best practices in action. Also, it enables you to acquire a step-by-step understanding of how to skillfully create each type.
|
||||||
|
|
||||||
|
### [Lead Generation Landing Page](landing/lead-generation)
|
||||||
|
|
||||||
|
**Purpose**: Designed primarily to capture user data, such as email addresses or contact details.
|
||||||
|
|
||||||
|
**Content**: Usually includes a form where users can input their details. It also highlights what they'll get in return, such as an eBook, a webinar, or a free trial.
|
||||||
|
|
||||||
|
**Focus**: Enticing visitors to provide their personal details by offering something valuable in return.
|
||||||
|
|
||||||
|
**Key Differentiator**: Unlike “Click-through Landing Pages,” which guide users to another step, these directly gather user data.
|
||||||
|
|
||||||
|
**Example**: A digital marketing agency offering a free SEO audit in exchange for business contact details.
|
||||||
|
|
||||||
|
### [Long-form Sales Landing Page](landing/sales)
|
||||||
|
|
||||||
|
**Purpose**: Primarily designed to sell, aiming to persuade and convert visitors into customers.
|
||||||
|
|
||||||
|
**Content**: Extensive, providing a wealth of information including product details, benefits, user stories, success stories, guarantees, and bonuses.
|
||||||
|
|
||||||
|
**Focus**: Utilizes a narrative to present a problem and offer the product or service as the solution. The aim is to emotionally connect with the visitor.
|
||||||
|
|
||||||
|
**Key Differentiator**: While 'Click-through Landing Pages' warm up the visitor for a bigger commitment. 'Long-form Sales Landing Pages' aim to close the sale directly on the page.
|
||||||
|
|
||||||
|
**Example**: A weight loss program detailing a person's journey and the challenges they've faced. It also highlights how the program assisted them and why it's an ideal solution for others.
|
||||||
|
|
||||||
|
### [Click-through Landing Page](landing/click-through)
|
||||||
|
|
||||||
|
**Purpose**: Acts as a middle step, warming up visitors for a bigger commitment.
|
||||||
|
|
||||||
|
**Content**: Provides essential details and benefits of an offer, urging visitors to click through to another page.
|
||||||
|
|
||||||
|
**Focus**: To lead visitors to the final conversion point, be it a checkout page or a sign-up form.
|
||||||
|
|
||||||
|
**Key Differentiator**: Unlike "Subscription Landing Pages" that aim for a recurring commitment, these lead to a one-time action.
|
||||||
|
|
||||||
|
**Example**: An online store showcasing a new product's benefits, leading visitors to the purchase page.
|
||||||
|
|
||||||
|
### [Product Details Landing Page](landing/product)
|
||||||
|
|
||||||
|
**Purpose**: Designed to inform by providing specific details about a product or service.
|
||||||
|
|
||||||
|
**Content**: Focuses on features, specifications, and benefits. May include high-quality images, detailed descriptions, demo videos, and user reviews.
|
||||||
|
|
||||||
|
**Focus**: Presents the product or service transparently and attractively.
|
||||||
|
|
||||||
|
**Key Differentiator**: While 'Long-form Sales Landing Pages' aim to persuade through narratives and overcoming objections. 'Product Details Landing Pages' focus on presenting the product or service in a clear and detailed manner.
|
||||||
|
|
||||||
|
**Example**: A tech website detailing a new laptop's specifications, unique features, comparisons with previous models, and user reviews.
|
||||||
|
|
||||||
|
### [Coming Soon or Pre-Launch Landing Page](landing/pre-launch)
|
||||||
|
|
||||||
|
**Purpose**: Creates excitement for an upcoming product, service, or event.
|
||||||
|
|
||||||
|
**Content**: Often includes a countdown timer, teaser content, and an option to sign up for notifications.
|
||||||
|
|
||||||
|
**Focus**: To generate buzz and capture early interest.
|
||||||
|
|
||||||
|
**Key Differentiator**: Unlike other landing pages that present available offers, these promote something not yet accessible.
|
||||||
|
|
||||||
|
**Example**: A game developer teasing their upcoming game release with sneak peeks and an option for early access.
|
||||||
|
|
||||||
|
### [Subscription Landing Page](landing/subscription)
|
||||||
|
|
||||||
|
**Purpose**: Encourages visitors to subscribe to a service, newsletter, or recurring product.
|
||||||
|
|
||||||
|
**Content**: Highlights the benefits of subscribing, often offering special deals or exclusive content for subscribers.
|
||||||
|
|
||||||
|
**Focus**: To secure a long-term commitment from the visitor.
|
||||||
|
|
||||||
|
**Key Differentiator**: Unlike "Click-through Landing Pages" that lead to a one-time action, these aim for a recurring commitment.
|
||||||
|
|
||||||
|
**Example**: A magazine promoting its monthly subscription, detailing exclusive articles and special subscriber-only benefits.
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
In the digital marketing symphony, Landing Pages become the crescendo. They capture attention, evoke action, and drive results. As we move forward, an essential task is to optimize, maintain relevance, and create high-converting Landing Pages. These factors collectively hold the key to achieving digital success.
|
||||||
|
|
||||||
|
Imagine a world where every online interaction gets personalized and directed. This showcases the potential of Landing Pages. For startups seeking traction or established brands introducing new products, Landing Pages can serve as the catalyst. They possess the power to spur digital growth and boost engagement.
|
204
v3/src/content/post/markdown-elements-demo-post.mdx
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
---
|
||||||
|
publishDate: 2023-01-02T00:00:00Z
|
||||||
|
title: Markdown elements demo post
|
||||||
|
excerpt: Sint sit cillum pariatur eiusmod nulla pariatur ipsum. Sit laborum anim qui mollit tempor pariatur nisi minim dolor.
|
||||||
|
tags:
|
||||||
|
- markdown
|
||||||
|
- blog
|
||||||
|
- Astro
|
||||||
|
---
|
||||||
|
|
||||||
|
import Logo from '~/components/Logo.astro';
|
||||||
|
import { YouTube, Tweet, Vimeo } from 'astro-embed';
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||||
|
|
||||||
|
## <a name="Headings"></a>Headings
|
||||||
|
|
||||||
|
Sint sit cillum pariatur eiusmod nulla pariatur ipsum. Sit laborum anim qui mollit tempor pariatur nisi minim dolor. Aliquip et adipisicing sit sit fugiat commodo id sunt. Nostrud enim ad commodo incididunt cupidatat in ullamco ullamco Lorem cupidatat velit enim et Lorem. Ut laborum cillum laboris fugiat culpa sint irure do reprehenderit culpa occaecat. Exercitation esse mollit tempor magna aliqua in occaecat aliquip veniam reprehenderit nisi dolor in laboris dolore velit.
|
||||||
|
|
||||||
|
## Heading two
|
||||||
|
|
||||||
|
Aute officia nulla deserunt do deserunt cillum velit magna. Officia veniam culpa anim minim dolore labore pariatur voluptate id ad est duis quis velit dolor pariatur enim. Incididunt enim excepteur do veniam consequat culpa do voluptate dolor fugiat ad adipisicing sit. Labore officia est adipisicing dolore proident eiusmod exercitation deserunt ullamco anim do occaecat velit. Elit dolor consectetur proident sunt aliquip est do tempor quis aliqua culpa aute. Duis in tempor exercitation pariatur et adipisicing mollit irure tempor ut enim esse commodo laboris proident. Do excepteur laborum anim esse aliquip eu sit id Lorem incididunt elit irure ea nulla dolor et. Nulla amet fugiat qui minim deserunt enim eu cupidatat aute officia do velit ea reprehenderit.
|
||||||
|
|
||||||
|
### Heading three
|
||||||
|
|
||||||
|
Voluptate cupidatat cillum elit quis ipsum eu voluptate fugiat consectetur enim. Quis ut voluptate culpa ex anim aute consectetur dolore proident voluptate exercitation eiusmod. Esse in do anim magna minim culpa sint. Adipisicing ipsum consectetur proident ullamco magna sit amet aliqua aute fugiat laborum exercitation duis et.
|
||||||
|
|
||||||
|
#### Heading four
|
||||||
|
|
||||||
|
Commodo fugiat aliqua minim quis pariatur mollit id tempor. Non occaecat minim esse enim aliqua adipisicing nostrud duis consequat eu adipisicing qui. Minim aliquip sit excepteur ipsum consequat laborum pariatur excepteur. Veniam fugiat et amet ad elit anim laborum duis mollit occaecat et et ipsum et reprehenderit. Occaecat aliquip dolore adipisicing sint labore occaecat officia fugiat. Quis adipisicing exercitation exercitation eu amet est laboris sunt nostrud ipsum reprehenderit ullamco. Enim sint ut consectetur id anim aute voluptate exercitation mollit dolore magna magna est Lorem. Ut adipisicing adipisicing aliqua ullamco voluptate labore nisi tempor esse magna incididunt.
|
||||||
|
|
||||||
|
##### Heading five
|
||||||
|
|
||||||
|
Veniam enim esse amet veniam deserunt laboris amet enim consequat. Minim nostrud deserunt cillum consectetur commodo eu enim nostrud ullamco occaecat excepteur. Aliquip et ut est commodo enim dolor amet sint excepteur. Amet ad laboris laborum deserunt sint sunt aliqua commodo ex duis deserunt enim est ex labore ut. Duis incididunt velit adipisicing non incididunt adipisicing adipisicing. Ad irure duis nisi tempor eu dolor fugiat magna et consequat tempor eu ex dolore. Mollit esse nisi qui culpa ut nisi ex proident culpa cupidatat cillum culpa occaecat anim. Ut officia sit ea nisi ea excepteur nostrud ipsum et nulla.
|
||||||
|
|
||||||
|
###### Heading six
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||||
|
|
||||||
|
[[Top]](#top)
|
||||||
|
|
||||||
|
## <a name="Paragraphs"></a>Paragraphs
|
||||||
|
|
||||||
|
Incididunt ex adipisicing ea ullamco consectetur in voluptate proident fugiat tempor deserunt reprehenderit ullamco id dolore laborum. Do laboris laboris minim incididunt qui consectetur exercitation adipisicing dolore et magna consequat magna anim sunt. Officia fugiat Lorem sunt pariatur incididunt Lorem reprehenderit proident irure. Dolore ipsum aliqua mollit ad officia fugiat sit eu aliquip cupidatat ipsum duis laborum laborum fugiat esse. Voluptate anim ex dolore deserunt ea ex eiusmod irure. Occaecat excepteur aliqua exercitation aliquip dolor esse eu eu.
|
||||||
|
|
||||||
|
Officia dolore laborum aute incididunt commodo nisi velit est est elit et dolore elit exercitation. Enim aliquip magna id ipsum aliquip consectetur ad nulla quis. Incididunt pariatur dolor consectetur cillum enim velit cupidatat laborum quis ex.
|
||||||
|
|
||||||
|
Officia irure in non voluptate adipisicing sit amet tempor duis dolore deserunt enim ut. Reprehenderit incididunt in ad anim et deserunt deserunt Lorem laborum quis. Enim aute anim labore proident laboris voluptate elit excepteur in. Ex labore nulla velit officia ullamco Lorem Lorem id do. Dolore ullamco ipsum magna dolor pariatur voluptate ipsum id occaecat ipsum. Dolore tempor quis duis commodo quis quis enim.
|
||||||
|
|
||||||
|
[[Top]](#top)
|
||||||
|
|
||||||
|
## <a name="Blockquotes"></a>Blockquotes
|
||||||
|
|
||||||
|
Ad nisi laborum aute cupidatat magna deserunt eu id laboris id. Aliquip nulla cupidatat sint ex Lorem mollit laborum dolor amet est ut esse aute. Nostrud ex consequat id incididunt proident ipsum minim duis aliqua ut ex et ad quis. Laborum sint esse cillum anim nulla cillum consectetur aliqua sit. Nisi excepteur cillum labore amet excepteur commodo enim occaecat consequat ipsum proident exercitation duis id in.
|
||||||
|
|
||||||
|
> Ipsum et cupidatat mollit exercitation enim duis sunt irure aliqua reprehenderit mollit. Pariatur Lorem pariatur laboris do culpa do elit irure. Eiusmod amet nulla voluptate velit culpa et aliqua ad reprehenderit sit ut.
|
||||||
|
|
||||||
|
Labore ea magna Lorem consequat aliquip consectetur cillum duis dolore. Et veniam dolor qui incididunt minim amet laboris sit. Dolore ad esse commodo et dolore amet est velit ut nisi ea. Excepteur ea nulla commodo dolore anim dolore adipisicing eiusmod labore id enim esse quis mollit deserunt est. Minim ea culpa voluptate nostrud commodo proident in duis aliquip minim.
|
||||||
|
|
||||||
|
> Qui est sit et reprehenderit aute est esse enim aliqua id aliquip ea anim. Pariatur sint reprehenderit mollit velit voluptate enim consectetur sint enim. Quis exercitation proident elit non id qui culpa dolore esse aliquip consequat.
|
||||||
|
|
||||||
|
Ipsum excepteur cupidatat sunt minim ad eiusmod tempor sit.
|
||||||
|
|
||||||
|
> Deserunt excepteur adipisicing culpa pariatur cillum laboris ullamco nisi fugiat cillum officia. In cupidatat nulla aliquip tempor ad Lorem Lorem quis voluptate officia consectetur pariatur ex in est duis. Mollit id esse est elit exercitation voluptate nostrud nisi laborum magna dolore dolore tempor in est consectetur.
|
||||||
|
|
||||||
|
Adipisicing voluptate ipsum culpa voluptate id aute laboris labore esse fugiat veniam ullamco occaecat do ut. Tempor et esse reprehenderit veniam proident ipsum irure sit ullamco et labore ea excepteur nulla labore ut. Ex aute minim quis tempor in eu id id irure ea nostrud dolor esse.
|
||||||
|
|
||||||
|
[[Top]](#top)
|
||||||
|
|
||||||
|
## <a name="Lists"></a>Lists
|
||||||
|
|
||||||
|
### Ordered List
|
||||||
|
|
||||||
|
1. Longan
|
||||||
|
2. Lychee
|
||||||
|
3. Excepteur ad cupidatat do elit laborum amet cillum reprehenderit consequat quis.
|
||||||
|
Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip aliquip velit pariatur dolore.
|
||||||
|
4. Marionberry
|
||||||
|
5. Melon
|
||||||
|
- Cantaloupe
|
||||||
|
- Honeydew
|
||||||
|
- Watermelon
|
||||||
|
6. Miracle fruit
|
||||||
|
7. Mulberry
|
||||||
|
|
||||||
|
### Unordered List
|
||||||
|
|
||||||
|
- Olive
|
||||||
|
- Orange
|
||||||
|
- Blood orange
|
||||||
|
- Clementine
|
||||||
|
- Papaya
|
||||||
|
- Ut aute ipsum occaecat nisi culpa Lorem id occaecat cupidatat id id magna laboris ad duis. Fugiat cillum dolore veniam nostrud proident sint consectetur eiusmod irure adipisicing.
|
||||||
|
- Passionfruit
|
||||||
|
|
||||||
|
[[Top]](#top)
|
||||||
|
|
||||||
|
## <a name="Horizontal"></a>Horizontal rule
|
||||||
|
|
||||||
|
In dolore velit aliquip labore mollit minim tempor veniam eu veniam ad in sint aliquip mollit mollit. Ex occaecat non deserunt elit laborum sunt tempor sint consequat culpa culpa qui sit. Irure ad commodo eu voluptate mollit cillum cupidatat veniam proident amet minim reprehenderit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
In laboris eiusmod reprehenderit aliquip sit proident occaecat. Non sit labore anim elit veniam Lorem minim commodo eiusmod irure do minim nisi. Dolor amet cillum excepteur consequat sint non sint.
|
||||||
|
|
||||||
|
[[Top]](#top)
|
||||||
|
|
||||||
|
## <a name="Table"></a>Table
|
||||||
|
|
||||||
|
Duis sunt ut pariatur reprehenderit mollit mollit magna dolore in pariatur nulla commodo sit dolor ad fugiat. Laboris amet ea occaecat duis eu enim exercitation deserunt ea laborum occaecat reprehenderit. Et incididunt dolor commodo consequat mollit nisi proident non pariatur in et incididunt id. Eu ut et Lorem ea ex magna minim ipsum ipsum do.
|
||||||
|
|
||||||
|
| Table Heading 1 | Table Heading 2 | Center align | Right align | Table Heading 5 |
|
||||||
|
| :-------------- | :-------------- | :----------: | ----------: | :-------------- |
|
||||||
|
| Item 1 | Item 2 | Item 3 | Item 4 | Item 5 |
|
||||||
|
| Item 1 | Item 2 | Item 3 | Item 4 | Item 5 |
|
||||||
|
| Item 1 | Item 2 | Item 3 | Item 4 | Item 5 |
|
||||||
|
| Item 1 | Item 2 | Item 3 | Item 4 | Item 5 |
|
||||||
|
| Item 1 | Item 2 | Item 3 | Item 4 | Item 5 |
|
||||||
|
|
||||||
|
Minim id consequat adipisicing cupidatat laborum culpa veniam non consectetur et duis pariatur reprehenderit eu ex consectetur. Sunt nisi qui eiusmod ut cillum laborum Lorem officia aliquip laboris ullamco nostrud laboris non irure laboris. Cillum dolore labore Lorem deserunt mollit voluptate esse incididunt ex dolor.
|
||||||
|
|
||||||
|
[[Top]](#top)
|
||||||
|
|
||||||
|
## <a name="Code"></a>Code
|
||||||
|
|
||||||
|
### Inline code
|
||||||
|
|
||||||
|
Ad amet irure est magna id mollit Lorem in do duis enim. Excepteur velit nisi magna ea pariatur pariatur ullamco fugiat deserunt sint non sint. Duis duis est `code in text` velit velit aute culpa ex quis pariatur pariatur laborum aute pariatur duis tempor sunt ad. Irure magna voluptate dolore consectetur consectetur irure esse. Anim magna `<strong>in culpa qui officia</strong>` dolor eiusmod esse amet aute cupidatat aliqua do id voluptate cupidatat reprehenderit amet labore deserunt.
|
||||||
|
|
||||||
|
### Highlighted
|
||||||
|
|
||||||
|
Et fugiat ad nisi amet magna labore do cillum fugiat occaecat cillum Lorem proident. In sint dolor ullamco ad do adipisicing amet id excepteur Lorem aliquip sit irure veniam laborum duis cillum. Aliqua occaecat minim cillum deserunt magna sunt laboris do do irure ea nostrud consequat ut voluptate ex.
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func handler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:])
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
http.HandleFunc("/", handler)
|
||||||
|
http.ListenAndServe(":8080", nil)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Ex amet id ex aliquip id do laborum excepteur exercitation elit sint commodo occaecat nostrud est. Nostrud pariatur esse veniam laborum non sint magna sit laboris minim in id. Aliqua pariatur pariatur excepteur adipisicing irure culpa consequat commodo et ex id ad.
|
||||||
|
|
||||||
|
[[Top]](#top)
|
||||||
|
|
||||||
|
## <a name="Inline"></a>Inline elements
|
||||||
|
|
||||||
|
Sint ea anim ipsum ad commodo cupidatat do **exercitation** incididunt et minim ad labore sunt. Minim deserunt labore laboris velit nulla incididunt ipsum nulla. Ullamco ad laborum ea qui et anim in laboris exercitation tempor sit officia laborum reprehenderit culpa velit quis. **Consequat commodo** reprehenderit duis [irure](#!) esse esse exercitation minim enim Lorem dolore duis irure. Nisi Lorem reprehenderit ea amet excepteur dolor excepteur magna labore proident voluptate ipsum. Reprehenderit ex esse deserunt aliqua ea officia mollit Lorem nulla magna enim. Et ad ipsum labore enim ipsum **cupidatat consequat**. Commodo non ea cupidatat magna deserunt dolore ipsum velit nulla elit veniam nulla eiusmod proident officia.
|
||||||
|
|
||||||
|
![Super wide](https://images.unsplash.com/photo-1710170601257-242514895755?q=80&w=2832&auto=format&fit=crop&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D)
|
||||||
|
|
||||||
|
_Proident sit veniam in est proident officia adipisicing_ ea tempor cillum non cillum velit deserunt. Voluptate laborum incididunt sit consectetur Lorem irure incididunt voluptate nostrud. Commodo ut eiusmod tempor cupidatat esse enim minim ex anim consequat. Mollit sint culpa qui laboris quis consectetur ad sint esse. Amet anim anim minim ullamco et duis non irure. Sit tempor adipisicing ea laboris `culpa ex duis sint` anim aute reprehenderit id eu ea. Aute [excepteur proident](#!) Lorem minim adipisicing nostrud mollit ad ut voluptate do nulla esse occaecat aliqua sint anim.
|
||||||
|
|
||||||
|
![Not so big](https://placehold.co/600x400/000000/FFFFFF/png)
|
||||||
|
|
||||||
|
Incididunt in culpa cupidatat mollit cillum qui proident sit. In cillum aliquip incididunt voluptate magna amet cupidatat cillum pariatur sint aliqua est _enim **anim** voluptate_. Magna aliquip proident incididunt id duis pariatur eiusmod incididunt commodo culpa dolore sit. Culpa do nostrud elit ad exercitation anim pariatur non minim nisi **adipisicing sunt _officia_**. Do deserunt magna mollit Lorem commodo ipsum do cupidatat mollit enim ut elit veniam ea voluptate.
|
||||||
|
|
||||||
|
Reprehenderit non eu quis in ad elit esse qui aute id [incididunt](#!) dolore cillum. Esse laboris consequat dolor anim exercitation tempor aliqua deserunt velit magna laboris. Culpa culpa minim duis amet mollit do quis amet commodo nulla irure.
|
||||||
|
|
||||||
|
[[Top]](#top)
|
||||||
|
|
||||||
|
## MDX
|
||||||
|
|
||||||
|
```js
|
||||||
|
---
|
||||||
|
publishDate: 'Aug 02 2022'
|
||||||
|
title: 'Markdown elements demo post'
|
||||||
|
---
|
||||||
|
import Logo from "~/components/Logo.astro";
|
||||||
|
|
||||||
|
## MDX
|
||||||
|
|
||||||
|
<Logo />
|
||||||
|
```
|
||||||
|
|
||||||
|
<div style="border:1px dashed;padding: 10px 5px">
|
||||||
|
<Logo />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Astro Embed
|
||||||
|
|
||||||
|
### Youtube
|
||||||
|
|
||||||
|
<YouTube id="y9n6HkftavM" />
|
||||||
|
|
||||||
|
### Tweet
|
||||||
|
|
||||||
|
<Tweet id="https://twitter.com/Steve8708/status/1598713161339015173" />
|
||||||
|
|
||||||
|
### Vimeo
|
||||||
|
|
||||||
|
<Vimeo id="178430038" />
|
||||||
|
|
||||||
|
[[Top]](#top)
|
62
v3/src/content/post/useful-resources-to-create-websites.md
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
---
|
||||||
|
publishDate: 2023-08-09T00:00:00Z
|
||||||
|
title: Useful tools and resources to create a professional website
|
||||||
|
excerpt: Explore vital tools and resources for a sleek website. From design to functionality, our guide elevates your online presence.
|
||||||
|
image: https://images.unsplash.com/photo-1637144113536-9c6e917be447?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1674&q=80
|
||||||
|
tags:
|
||||||
|
- front-end
|
||||||
|
- tools
|
||||||
|
- resources
|
||||||
|
---
|
||||||
|
|
||||||
|
## Magna nunc senectus torquent per fusce sapien ligula tempus cra
|
||||||
|
|
||||||
|
Lorem ipsum dolor sit amet consectetur adipiscing, elit fusce imperdiet gravida velit massa, ligula aenean suscipit sociis lacinia. Sapien scelerisque rutrum sem accumsan orci imperdiet aliquam inceptos aliquet tempus ornare, netus nostra nam nunc platea pulvinar urna et suscipit pellentesque, aenean congue sociis non tellus quis proin etiam venenatis pretium. Nibh senectus lacinia volutpat nostra taciti ac posuere, dictum ultricies dictumst luctus in vehicula, mus molestie venenatis penatibus ridiculus elementum. Phasellus sollicitudin dignissim parturient tempor cubilia erat massa eleifend dapibus, condimentum cras tortor eu sem dictumst non. Faucibus neque est malesuada nostra luctus maecenas at condimentum, arcu eros vulputate curabitur blandit mollis volutpat, lectus leo dictumst duis semper tempor hendrerit. Egestas scelerisque fusce torquent cubilia consequat conubia lacus et mollis, condimentum taciti elementum sapien risus vulputate est.
|
||||||
|
|
||||||
|
Tristique eleifend enim praesent mollis sem leo, molestie dictum penatibus sodales consequat ligula nulla, platea feugiat aptent sapien turpis. Mollis mus ac taciti maecenas pretium hendrerit proin accumsan, mattis dictumst netus nunc facilisi morbi cursus euismod quis, a commodo nulla integer varius enim vehicula. Consequat mi risus vulputate ullamcorper sociosqu pretium molestie cursus, parturient viverra non tempor tellus convallis vitae eleifend mus, bibendum pellentesque imperdiet vivamus nunc phasellus iaculis. Volutpat est ac dictumst eleifend maecenas torquent quam hac, mollis aliquam mattis euismod ornare risus fringilla proin nisi, sem fermentum primis ultrices varius etiam id. Posuere nunc mus curabitur condimentum lobortis euismod donec tincidunt ridiculus, tristique senectus cum taciti quam blandit leo malesuada, sociis nullam cras litora sem laoreet sed nec.
|
||||||
|
|
||||||
|
## Magna lacus tortor luctus platea co
|
||||||
|
|
||||||
|
- Luctus molestie taciti aliquam dictumst imperdiet, donec torquent nisi.
|
||||||
|
|
||||||
|
- Montes cursus habitant risus platea senectus, lectus sagittis mi.
|
||||||
|
|
||||||
|
- Eleifend facilisi quam ultricies, accumsan aliquet euismod velit, sem tortor.
|
||||||
|
|
||||||
|
- Senectus nisl potenti congue sociosqu at, porttitor habitant vivamus.
|
||||||
|
|
||||||
|
Nostra dictum porta consequat quisque diam nisl iaculis velit varius, placerat curabitur risus commodo condimentum morbi eros dictumst phasellus, tempor duis libero ultrices est rhoncus mattis nam. Quisque lectus massa lobortis nulla enim, praesent eu ut elementum. Felis placerat nibh donec erat platea rutrum taciti cursus, elementum metus semper feugiat risus tellus nulla, aliquam hendrerit faucibus inceptos commodo justo porta.
|
||||||
|
|
||||||
|
Cursus imperdiet montes natoque potenti suscipit facilisi porta mollis posuere consequat, aliquam turpis tortor libero viverra rhoncus accumsan inceptos blandit convallis diam, penatibus ut pretium in duis leo auctor proin quisque. Sollicitudin inceptos quam molestie sociis habitasse class sapien vivamus facilisis, consequat ante vehicula velit tempor cum rutrum magnis, eget semper quisque turpis pretium praesent per faucibus. Ullamcorper blandit taciti primis sed pharetra inceptos duis, eu nisi ac fringilla tellus accumsan iaculis, morbi integer at purus hac est. Elementum hac lacus per in posuere erat ad, egestas dapibus malesuada suscipit nunc interdum, mi risus auctor pretium lectus massa. Condimentum nullam molestie tincidunt sodales luctus parturient est et congue hendrerit vel vulputate iaculis, curabitur sollicitudin quisque magna nostra nisl nam massa viverra donec neque class.
|
||||||
|
|
||||||
|
Euismod tempus potenti interdum fusce placerat habitant, taciti turpis faucibus curabitur tempor felis porta, sed aenean mi arcu magnis. Pellentesque tincidunt aptent eget nisi convallis lobortis sapien, habitasse sollicitudin proin vehicula ridiculus duis congue, himenaeos lectus vitae nulla taciti ante. Enim commodo non taciti ultricies donec iaculis aliquet interdum, dictumst a pulvinar lacus cursus fames praesent cras ad, rutrum nostra dis accumsan primis euismod sagittis. Eu habitant euismod mattis at congue fusce ad commodo litora himenaeos aenean, porta lobortis suscipit pulvinar magna facilisi nullam ante non senectus, urna volutpat sodales vitae varius lectus tincidunt montes rutrum vulputate. Enim cum habitant morbi maecenas nisl imperdiet a egestas velit, gravida laoreet hendrerit rutrum molestie fames sapien euismod turpis metus, faucibus class sed primis leo nam malesuada fermentum.
|
||||||
|
|
||||||
|
Semper etiam tellus a risus lobortis dictumst sem massa eros, eget curae gravida accumsan hac parturient nulla fringilla convallis, condimentum torquent placerat mauris conubia augue mattis leo. Dictum tincidunt quis risus volutpat netus mi suscipit parturient suspendisse vestibulum, ad lacus dictumst luctus nec fusce ultricies vivamus. Dui sociis nulla suscipit gravida mi arcu netus, vitae mus donec dapibus nascetur id ante urna, egestas viverra auctor sodales litora enim rutrum, sapien molestie imperdiet ut massa elementum. Aptent ante risus erat malesuada nec porta, ligula nascetur dictum nunc turpis natoque, tristique conubia netus arcu a.
|
||||||
|
|
||||||
|
Nam scelerisque ridiculus suspendisse viverra conubia et fermentum nascetur turpis quisque, vestibulum cubilia curae per feugiat lectus rhoncus suscipit neque. Urna habitasse mus hac fringilla rutrum sodales, nullam aliquam porttitor quis vehicula arcu class, in felis placerat mattis vestibulum habitant, mauris eros dapibus penatibus viverra. Senectus tristique molestie scelerisque quisque mus augue facilisi massa, ac viverra dapibus vehicula nostra vel nam, posuere montes parturient auctor eu ultrices natoque. Quam fringilla volutpat morbi in per aliquet laoreet a maecenas, lacus velit mauris purus ultricies sociosqu pulvinar netus sodales, convallis placerat turpis tellus nullam libero leo aptent. Praesent lacus ultricies per ligula taciti aenean conubia parturient, feugiat sodales viverra urna cubilia etiam nibh curabitur congue, tristique nisl at elementum dis natoque commodo.
|
||||||
|
|
||||||
|
Sagittis erat velit integer cursus congue viverra, conubia himenaeos egestas ultricies praesent, hac litora mattis non venenatis. Duis libero morbi curae potenti litora vitae sed etiam consequat magna ultricies, magnis fermentum vehicula feugiat tortor ad quis orci rhoncus per porta, ante mi gravida dis nostra tempor lobortis aenean convallis molestie. Ligula fusce blandit ac accumsan magnis rutrum nostra velit maecenas, netus lobortis himenaeos purus justo sapien posuere libero, cum etiam urna mi ultrices est sociis tortor. Neque inceptos quisque vestibulum tempor phasellus id himenaeos magna suspendisse a in nunc cursus, morbi dignissim ornare non auctor massa iaculis mus nec elementum ultricies maecenas. Scelerisque maecenas ultrices integer gravida dis cursus, sed at semper libero iaculis varius, justo augue nec tincidunt suspendisse.
|
||||||
|
|
||||||
|
Rutrum augue natoque felis non vestibulum nam duis, quam praesent taciti himenaeos class vel dis rhoncus, dapibus pulvinar etiam ridiculus curae nibh blandit, scelerisque cursus nostra pretium suspendisse vehicula. Etiam sem metus eleifend suscipit felis suspendisse ut, velit fames habitant semper placerat fusce cras, nunc venenatis platea aenean euismod libero. In eu eget pellentesque libero egestas suspendisse quis tristique torquent nulla, magnis dis malesuada purus quam platea aliquet tortor odio. Accumsan nostra augue lobortis elementum justo sociosqu posuere aptent est, nisl metus conubia tellus sollicitudin lacus inceptos. Morbi mauris aenean malesuada arcu fusce libero venenatis commodo iaculis litora dis, erat parturient class sed facilisi mus a nec dictum.
|
||||||
|
|
||||||
|
Senectus platea dapibus volutpat dictum pharetra cursus netus cras, arcu sociis ornare potenti porttitor tempus sollicitudin, ullamcorper duis nam convallis sapien pretium conubia. Mi metus vivamus cum id semper fringilla senectus scelerisque pretium placerat sociis rhoncus pulvinar porttitor accumsan, curae ligula fermentum mus hendrerit ridiculus condimentum per suscipit rutrum sociosqu odio pellentesque suspendisse. Dui massa nulla suscipit duis metus mollis pellentesque, scelerisque posuere interdum ligula cum dignissim sed, placerat ante ultrices mi netus augue. Eu porttitor malesuada diam morbi torquent egestas magnis tempus metus imperdiet nisl, ad sociis lectus neque mauris gravida habitant primis lobortis. Phasellus mattis nulla fames parturient pharetra pretium egestas, diam rhoncus placerat lectus maecenas dictumst sed cum, justo non ac volutpat morbi enim.
|
||||||
|
|
||||||
|
Justo fringilla morbi netus habitasse varius primis eu magna, tristique accumsan mus enim lectus cubilia convallis auctor, nunc imperdiet erat mollis rutrum vel turpis. Justo purus laoreet eros turpis interdum et ridiculus torquent integer nunc, himenaeos eu tellus proin scelerisque tincidunt congue posuere ultricies vestibulum auctor, aliquet semper varius placerat imperdiet non nisl cubilia fermentum. Feugiat nisl himenaeos cum metus mi est ac, euismod elementum velit tempus dictum mauris, bibendum faucibus cubilia phasellus nulla ornare. Etiam justo venenatis varius laoreet sociis montes dignissim, elementum ligula malesuada euismod praesent magnis auctor, eleifend class egestas a vestibulum blandit. Scelerisque potenti facilisis torquent mollis nisi felis et sed, aptent tortor platea non quisque nec accumsan inceptos, velit molestie nunc enim cubilia egestas per.
|
||||||
|
|
||||||
|
Ultrices morbi et potenti eros aenean condimentum magnis est felis porta, dictumst taciti inceptos etiam ultricies cubilia hac torquent tempor vulputate, sodales erat semper vestibulum dignissim sociis viverra suscipit sagittis. Justo non auctor penatibus iaculis sed in volutpat pretium feugiat lectus rutrum, curabitur sociosqu sapien semper a laoreet augue primis fringilla dui. Fringilla iaculis blandit feugiat euismod congue morbi erat eros, mi dis egestas facilisi volutpat risus cras porta, orci vivamus turpis conubia est commodo torquent. Lectus euismod maecenas potenti in ac natoque sed ullamcorper ridiculus, diam fringilla condimentum eget convallis hendrerit varius pellentesque. Feugiat cras nullam tristique leo nisl dignissim lacinia aenean vivamus potenti consequat, vulputate curabitur sed risus mus suspendisse litora sollicitudin tempor.
|
||||||
|
|
||||||
|
Egestas hac arcu dapibus placerat proin aptent a pellentesque posuere, in condimentum fames facilisi maecenas semper nisl mus, sodales donec elementum praesent enim ac dictum ridiculus. Justo in nibh luctus vitae etiam nisl ac quisque fringilla, habitasse sociosqu curae inceptos semper ut mi hac, congue volutpat himenaeos sed augue morbi tellus nec. Congue libero posuere varius eleifend tristique nascetur integer ullamcorper, est leo vitae mi erat enim augue urna magnis, elementum ultricies pulvinar blandit arcu malesuada duis. Cubilia nulla vel et integer sed pellentesque gravida felis pulvinar mollis ultricies mi, montes suspendisse vestibulum aliquet dui in magna nunc ridiculus aliquam elementum. Justo erat montes enim felis eu sed vivamus faucibus imperdiet ac luctus vulputate, cursus accumsan blandit et mus sodales conubia cubilia phasellus leo.
|
||||||
|
|
||||||
|
Velit in felis penatibus semper laoreet libero tristique condimentum sem montes suscipit, morbi habitant gravida tellus quisque neque torquent lobortis interdum. Ridiculus sollicitudin suscipit semper quam eleifend at, neque tincidunt magnis penatibus dui orci, praesent vulputate himenaeos feugiat vel. Habitasse senectus a sodales dapibus nulla auctor sagittis nullam molestie, imperdiet volutpat quam odio facilisis nostra magnis dictumst, sociis cum erat facilisi dignissim urna lacus magna. Primis porttitor nullam quis vestibulum mi dictumst magna dapibus taciti magnis inceptos fames, purus etiam auctor metus bibendum felis accumsan id aliquet suscipit imperdiet. Pellentesque sem velit nulla consequat vehicula cubilia curabitur, platea curae natoque tristique nullam litora, nascetur imperdiet habitant tincidunt suspendisse sociis.
|
||||||
|
|
||||||
|
Platea cum auctor eget consequat elementum lacinia ad aliquet orci, imperdiet nibh penatibus ac dictum rutrum mollis ante cursus, volutpat scelerisque velit ornare in vivamus pharetra blandit. Cum mattis interdum in diam purus sapien lacinia gravida, semper montes vestibulum rhoncus auctor morbi dictum. Mus semper erat mollis taciti sapien ultrices accumsan ante magna eros at commodo, malesuada diam nullam massa curabitur lobortis felis interdum nisi duis pellentesque. Accumsan faucibus tristique augue enim hac ante feugiat, porttitor phasellus condimentum nulla maecenas dignissim at platea, facilisis nam donec primis habitasse ac. Nec convallis ridiculus potenti primis faucibus erat eget metus mollis, luctus ac fusce condimentum orci suscipit volutpat malesuada mi, velit feugiat pharetra sem turpis est accumsan porta, ligula torquent lacus tristique a senectus tortor dignissim.
|
||||||
|
|
||||||
|
Pharetra eleifend vivamus potenti congue proin himenaeos, fusce mi venenatis natoque montes, suscipit commodo porta magnis mattis. Et lobortis mollis libero quis himenaeos felis dis porta, donec iaculis mattis cursus accumsan pulvinar mus etiam, habitasse leo taciti vitae suscipit suspendisse bibendum. Sodales at ante dictumst nostra est risus senectus semper morbi facilisis neque tempus, venenatis penatibus fusce mattis phasellus velit diam iaculis hac tortor class, orci ridiculus varius dis odio cras rutrum porttitor facilisi massa parturient. Augue facilisi nam proin at elementum massa, tellus vestibulum mattis tortor porta, cubilia sodales orci congue vel. Rhoncus nec quam iaculis sapien risus suspendisse dictum tincidunt, vivamus lobortis blandit metus ullamcorper torquent.
|
||||||
|
|
||||||
|
Ante fermentum hac tincidunt nam sodales vestibulum pellentesque ut nulla habitasse, ornare diam facilisis aptent facilisi penatibus arcu congue lacus, lectus fringilla per primis dapibus eu imperdiet erat dictumst. Pulvinar eu ad mauris nulla ac sed nisl ullamcorper natoque etiam fames, platea aliquam dis netus odio dignissim tincidunt quam blandit laoreet, at mollis ridiculus molestie lacus metus nullam suspendisse nibh duis. Suspendisse congue vestibulum fringilla ridiculus tristique sagittis sociosqu integer, volutpat lacinia pulvinar felis aliquam pharetra faucibus dictumst ad, fusce dignissim cursus mauris eget nostra lectus. Lacinia egestas iaculis scelerisque odio gravida ullamcorper, at arcu ligula ornare parturient phasellus laoreet, augue convallis platea tortor aenean.
|
||||||
|
|
||||||
|
Interdum fames lobortis sollicitudin aliquet mus aptent netus, penatibus consequat pulvinar velit enim curae accumsan, maecenas litora mi rutrum sagittis tincidunt. Lacinia malesuada id netus suscipit sapien sociosqu orci habitasse turpis, feugiat donec placerat sed quam hendrerit pellentesque. Erat accumsan ligula id sapien turpis mus nulla lobortis consequat nec, urna habitasse ultrices aliquet vulputate est suspendisse gravida senectus odio, vehicula fusce proin in sed tempor vitae convallis molestie. Nascetur semper feugiat velit hendrerit lacinia nunc, risus quis congue nullam himenaeos commodo porttitor, natoque facilisi ad maecenas faucibus. Dictum id sodales interdum accumsan habitant natoque class parturient mi venenatis aenean, est nam tortor donec lobortis non vehicula magnis lacinia. Feugiat vitae morbi litora vehicula in a, nam ad ultrices auctor sollicitudin, ullamcorper fringilla hendrerit placerat faucibus.
|
||||||
|
|
||||||
|
Nulla nisi ac placerat duis semper mus cursus interdum netus vestibulum, tortor praesent proin nec rhoncus magnis commodo blandit himenaeos purus, volutpat id montes scelerisque suspendisse risus nisl erat dui. Senectus et habitant dis nulla velit faucibus venenatis sapien, dapibus etiam metus eget magnis feugiat tristique. Augue montes elementum pulvinar mollis pellentesque diam cursus tristique vel cubilia erat mus, congue curae sagittis dui quis fusce tortor consequat taciti natoque. Praesent montes erat feugiat sed euismod condimentum potenti malesuada nec, mi vitae suspendisse aptent senectus eleifend faucibus pulvinar scelerisque, augue ornare accumsan pretium magna eu iaculis metus. Suscipit accumsan massa vitae platea ad duis rhoncus fermentum vulputate, interdum pretium metus per aptent enim in facilisis eros, sollicitudin consequat iaculis erat dictumst quisque leo sociis.
|
||||||
|
|
||||||
|
Tempor etiam potenti auctor est ut habitant ac nisl ultrices pulvinar, sem primis tempus lacus aliquam consequat fringilla tristique. Consequat cum rhoncus massa sociis blandit rutrum nisi quam cras vitae fusce, sociosqu erat penatibus convallis fames accumsan eros himenaeos pulvinar sagittis, habitasse primis integer odio nascetur in montes faucibus semper potenti. Diam aliquam fringilla risus phasellus habitasse aenean eu erat, netus nulla pellentesque ut morbi torquent pharetra semper, sed etiam primis in conubia hendrerit velit. Ornare magna dictum purus metus sociosqu pulvinar sed, quam faucibus posuere pretium senectus interdum. Ornare sodales in litora nascetur sociosqu senectus auctor, cras arcu fusce ac inceptos integer tempor aliquam, tristique imperdiet metus hendrerit erat eleifend.
|
5
v3/src/env.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
|
||||||
|
/// <reference path="../.astro/types.d.ts" />
|
||||||
|
/// <reference types="astro/client" />
|
||||||
|
/// <reference types="vite/client" />
|
||||||
|
/// <reference types="../vendor/integration/types.d.ts" />
|
35
v3/src/layouts/LandingLayout.astro
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
import PageLayout from '~/layouts/PageLayout.astro';
|
||||||
|
import Header from '~/components/widgets/Header.astro';
|
||||||
|
|
||||||
|
import { headerData } from '~/navigation';
|
||||||
|
import type { MetaData } from '~/types';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
metadata?: MetaData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { metadata } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<PageLayout metadata={metadata}>
|
||||||
|
<Fragment slot="announcement">
|
||||||
|
<slot name="announcement" />
|
||||||
|
</Fragment>
|
||||||
|
<Fragment slot="header">
|
||||||
|
<slot name="header">
|
||||||
|
<Header
|
||||||
|
links={headerData?.links[2] ? [headerData.links[2]] : undefined}
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
text: 'Download',
|
||||||
|
href: 'https://github.com/onwidget/astrowind',
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
showToggleTheme
|
||||||
|
position="right"
|
||||||
|
/>
|
||||||
|
</slot>
|
||||||
|
</Fragment>
|
||||||
|
<slot />
|
||||||
|
</PageLayout>
|
48
v3/src/layouts/Layout.astro
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
import '~/assets/styles/tailwind.css';
|
||||||
|
|
||||||
|
import { I18N } from 'astrowind:config';
|
||||||
|
|
||||||
|
import CommonMeta from '~/components/common/CommonMeta.astro';
|
||||||
|
import Favicons from '~/components/Favicons.astro';
|
||||||
|
import CustomStyles from '~/components/CustomStyles.astro';
|
||||||
|
import ApplyColorMode from '~/components/common/ApplyColorMode.astro';
|
||||||
|
import Metadata from '~/components/common/Metadata.astro';
|
||||||
|
import SiteVerification from '~/components/common/SiteVerification.astro';
|
||||||
|
import Analytics from '~/components/common/Analytics.astro';
|
||||||
|
import BasicScripts from '~/components/common/BasicScripts.astro';
|
||||||
|
|
||||||
|
// Comment the line below to disable View Transitions
|
||||||
|
import { ViewTransitions } from 'astro:transitions';
|
||||||
|
|
||||||
|
import type { MetaData as MetaDataType } from '~/types';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
metadata?: MetaDataType;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { metadata = {} } = Astro.props;
|
||||||
|
const { language, textDirection } = I18N;
|
||||||
|
---
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang={language} dir={textDirection} class="2xl:text-[20px]">
|
||||||
|
<head>
|
||||||
|
<CommonMeta />
|
||||||
|
<Favicons />
|
||||||
|
<CustomStyles />
|
||||||
|
<ApplyColorMode />
|
||||||
|
<Metadata {...metadata} />
|
||||||
|
<SiteVerification />
|
||||||
|
<Analytics />
|
||||||
|
|
||||||
|
<!-- Comment the line below to disable View Transitions -->
|
||||||
|
<ViewTransitions fallback="swap" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="antialiased text-default bg-page tracking-tight">
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
<BasicScripts />
|
||||||
|
</body>
|
||||||
|
</html>
|
28
v3/src/layouts/MarkdownLayout.astro
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
import Layout from '~/layouts/PageLayout.astro';
|
||||||
|
|
||||||
|
import type { MetaData } from '~/types';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
frontmatter: {
|
||||||
|
title?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { frontmatter } = Astro.props;
|
||||||
|
|
||||||
|
const metadata: MetaData = {
|
||||||
|
title: frontmatter?.title,
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout metadata={metadata}>
|
||||||
|
<section class="px-4 py-16 sm:px-6 mx-auto lg:px-8 lg:py-20 max-w-4xl">
|
||||||
|
<h1 class="font-bold font-heading text-4xl md:text-5xl leading-tighter tracking-tighter">{frontmatter.title}</h1>
|
||||||
|
<div
|
||||||
|
class="mx-auto prose prose-lg max-w-4xl dark:prose-invert dark:prose-headings:text-slate-300 prose-md prose-headings:font-heading prose-headings:leading-tighter prose-headings:tracking-tighter prose-headings:font-bold prose-a:text-blue-600 dark:prose-a:text-blue-400 prose-img:rounded-md prose-img:shadow-lg mt-8"
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</Layout>
|
31
v3/src/layouts/PageLayout.astro
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
import Layout from '~/layouts/Layout.astro';
|
||||||
|
import Header from '~/components/widgets/Header.astro';
|
||||||
|
import Footer from '~/components/widgets/Footer.astro';
|
||||||
|
import Announcement from '~/components/widgets/Announcement.astro';
|
||||||
|
|
||||||
|
import { headerData, footerData } from '~/navigation';
|
||||||
|
|
||||||
|
import type { MetaData } from '~/types';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
metadata?: MetaData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { metadata } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout metadata={metadata}>
|
||||||
|
<slot name="announcement">
|
||||||
|
<Announcement />
|
||||||
|
</slot>
|
||||||
|
<slot name="header">
|
||||||
|
<Header {...headerData} isSticky showRssFeed showToggleTheme />
|
||||||
|
</slot>
|
||||||
|
<main>
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
<slot name="footer">
|
||||||
|
<Footer {...footerData} />
|
||||||
|
</slot>
|
||||||
|
</Layout>
|
183
v3/src/navigation.ts
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import { getPermalink, getBlogPermalink, getAsset } from './utils/permalinks';
|
||||||
|
|
||||||
|
export const headerData = {
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
text: 'Homes',
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
text: 'SaaS',
|
||||||
|
href: getPermalink('/homes/saas'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Startup',
|
||||||
|
href: getPermalink('/homes/startup'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Mobile App',
|
||||||
|
href: getPermalink('/homes/mobile-app'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Personal',
|
||||||
|
href: getPermalink('/homes/personal'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Pages',
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
text: 'Features (Anchor Link)',
|
||||||
|
href: getPermalink('/#features'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Services',
|
||||||
|
href: getPermalink('/services'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Pricing',
|
||||||
|
href: getPermalink('/pricing'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'About us',
|
||||||
|
href: getPermalink('/about'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Contact',
|
||||||
|
href: getPermalink('/contact'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Terms',
|
||||||
|
href: getPermalink('/terms'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Privacy policy',
|
||||||
|
href: getPermalink('/privacy'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Landing',
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
text: 'Lead Generation',
|
||||||
|
href: getPermalink('/landing/lead-generation'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Long-form Sales',
|
||||||
|
href: getPermalink('/landing/sales'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Click-Through',
|
||||||
|
href: getPermalink('/landing/click-through'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Product Details (or Services)',
|
||||||
|
href: getPermalink('/landing/product'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Coming Soon or Pre-Launch',
|
||||||
|
href: getPermalink('/landing/pre-launch'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Subscription',
|
||||||
|
href: getPermalink('/landing/subscription'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Blog',
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
text: 'Blog List',
|
||||||
|
href: getBlogPermalink(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Article',
|
||||||
|
href: getPermalink('get-started-website-with-astro-tailwind-css', 'post'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Article (with MDX)',
|
||||||
|
href: getPermalink('markdown-elements-demo-post', 'post'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Category Page',
|
||||||
|
href: getPermalink('tutorials', 'category'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Tag Page',
|
||||||
|
href: getPermalink('astro', 'tag'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Widgets',
|
||||||
|
href: '#',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
actions: [{ text: 'Download', href: 'https://github.com/onwidget/astrowind', target: '_blank' }],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const footerData = {
|
||||||
|
links: [
|
||||||
|
{
|
||||||
|
title: 'Product',
|
||||||
|
links: [
|
||||||
|
{ text: 'Features', href: '#' },
|
||||||
|
{ text: 'Security', href: '#' },
|
||||||
|
{ text: 'Team', href: '#' },
|
||||||
|
{ text: 'Enterprise', href: '#' },
|
||||||
|
{ text: 'Customer stories', href: '#' },
|
||||||
|
{ text: 'Pricing', href: '#' },
|
||||||
|
{ text: 'Resources', href: '#' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Platform',
|
||||||
|
links: [
|
||||||
|
{ text: 'Developer API', href: '#' },
|
||||||
|
{ text: 'Partners', href: '#' },
|
||||||
|
{ text: 'Atom', href: '#' },
|
||||||
|
{ text: 'Electron', href: '#' },
|
||||||
|
{ text: 'AstroWind Desktop', href: '#' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Support',
|
||||||
|
links: [
|
||||||
|
{ text: 'Docs', href: '#' },
|
||||||
|
{ text: 'Community Forum', href: '#' },
|
||||||
|
{ text: 'Professional Services', href: '#' },
|
||||||
|
{ text: 'Skills', href: '#' },
|
||||||
|
{ text: 'Status', href: '#' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Company',
|
||||||
|
links: [
|
||||||
|
{ text: 'About', href: '#' },
|
||||||
|
{ text: 'Blog', href: '#' },
|
||||||
|
{ text: 'Careers', href: '#' },
|
||||||
|
{ text: 'Press', href: '#' },
|
||||||
|
{ text: 'Inclusion', href: '#' },
|
||||||
|
{ text: 'Social Impact', href: '#' },
|
||||||
|
{ text: 'Shop', href: '#' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
secondaryLinks: [
|
||||||
|
{ text: 'Terms', href: getPermalink('/terms') },
|
||||||
|
{ text: 'Privacy Policy', href: getPermalink('/privacy') },
|
||||||
|
],
|
||||||
|
socialLinks: [
|
||||||
|
{ ariaLabel: 'X', icon: 'tabler:brand-x', href: '#' },
|
||||||
|
{ ariaLabel: 'Instagram', icon: 'tabler:brand-instagram', href: '#' },
|
||||||
|
{ ariaLabel: 'Facebook', icon: 'tabler:brand-facebook', href: '#' },
|
||||||
|
{ ariaLabel: 'RSS', icon: 'tabler:rss', href: getAsset('/rss.xml') },
|
||||||
|
{ ariaLabel: 'Github', icon: 'tabler:brand-github', href: 'https://github.com/onwidget/astrowind' },
|
||||||
|
],
|
||||||
|
footNote: `
|
||||||
|
<img class="w-5 h-5 md:w-6 md:h-6 md:-mt-0.5 bg-cover mr-1.5 rtl:mr-0 rtl:ml-1.5 float-left rtl:float-right rounded-sm" src="https://onwidget.com/favicon/favicon-32x32.png" alt="onWidget logo" loading="lazy"></img>
|
||||||
|
Made by <a class="text-blue-600 underline dark:text-muted" href="https://onwidget.com/"> onWidget</a> · All rights reserved.
|
||||||
|
`,
|
||||||
|
};
|
24
v3/src/pages/404.astro
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
import Layout from '~/layouts/Layout.astro';
|
||||||
|
import { getHomePermalink } from '~/utils/permalinks';
|
||||||
|
|
||||||
|
const title = `Error 404`;
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout metadata={{ title }}>
|
||||||
|
<section class="flex items-center h-full p-16">
|
||||||
|
<div class="container flex flex-col items-center justify-center px-5 mx-auto my-8">
|
||||||
|
<div class="max-w-md text-center">
|
||||||
|
<h2 class="mb-8 font-bold text-9xl">
|
||||||
|
<span class="sr-only">Error</span>
|
||||||
|
<span class="text-primary">404</span>
|
||||||
|
</h2>
|
||||||
|
<p class="text-3xl font-semibold md:text-3xl">Sorry, we couldn't find this page.</p>
|
||||||
|
<p class="mt-4 mb-8 text-lg text-muted dark:text-slate-400">
|
||||||
|
But dont worry, you can find plenty of other things on our homepage.
|
||||||
|
</p>
|
||||||
|
<a rel="noopener noreferrer" href={getHomePermalink()} class="btn ml-4">Back to homepage</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</Layout>
|
52
v3/src/pages/[...blog]/[...page].astro
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
import type { InferGetStaticPropsType, GetStaticPaths } from 'astro';
|
||||||
|
|
||||||
|
import Layout from '~/layouts/PageLayout.astro';
|
||||||
|
import BlogList from '~/components/blog/List.astro';
|
||||||
|
import Headline from '~/components/blog/Headline.astro';
|
||||||
|
import Pagination from '~/components/blog/Pagination.astro';
|
||||||
|
// import PostTags from "~/components/blog/Tags.astro";
|
||||||
|
|
||||||
|
import { blogListRobots, getStaticPathsBlogList } from '~/utils/blog';
|
||||||
|
|
||||||
|
export const prerender = true;
|
||||||
|
|
||||||
|
export const getStaticPaths = (async ({ paginate }) => {
|
||||||
|
return await getStaticPathsBlogList({ paginate });
|
||||||
|
}) satisfies GetStaticPaths;
|
||||||
|
|
||||||
|
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
|
||||||
|
|
||||||
|
const { page } = Astro.props as Props;
|
||||||
|
const currentPage = page.currentPage ?? 1;
|
||||||
|
|
||||||
|
// const allCategories = await findCategories();
|
||||||
|
// const allTags = await findTags();
|
||||||
|
|
||||||
|
const metadata = {
|
||||||
|
title: `Blog${currentPage > 1 ? ` — Page ${currentPage}` : ''}`,
|
||||||
|
robots: {
|
||||||
|
index: blogListRobots?.index && currentPage === 1,
|
||||||
|
follow: blogListRobots?.follow,
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
type: 'blog',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout metadata={metadata}>
|
||||||
|
<section class="px-6 sm:px-6 py-12 sm:py-16 lg:py-20 mx-auto max-w-4xl">
|
||||||
|
<Headline
|
||||||
|
subtitle="A statically generated blog example with news, tutorials, resources and other interesting content related to AstroWind"
|
||||||
|
>
|
||||||
|
The Blog
|
||||||
|
</Headline>
|
||||||
|
<BlogList posts={page.data} />
|
||||||
|
<Pagination prevUrl={page.url.prev} nextUrl={page.url.next} />
|
||||||
|
<!--
|
||||||
|
<PostTags tags={allCategories} class="mb-2" title="Search by Categories:" isCategory />
|
||||||
|
<PostTags tags={allTags} title="Search by Tags:" />
|
||||||
|
-->
|
||||||
|
</section>
|
||||||
|
</Layout>
|
37
v3/src/pages/[...blog]/[category]/[...page].astro
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
import type { InferGetStaticPropsType, GetStaticPaths } from 'astro';
|
||||||
|
import { blogCategoryRobots, getStaticPathsBlogCategory } from '~/utils/blog';
|
||||||
|
|
||||||
|
import Layout from '~/layouts/PageLayout.astro';
|
||||||
|
import BlogList from '~/components/blog/List.astro';
|
||||||
|
import Headline from '~/components/blog/Headline.astro';
|
||||||
|
import Pagination from '~/components/blog/Pagination.astro';
|
||||||
|
|
||||||
|
export const prerender = true;
|
||||||
|
|
||||||
|
export const getStaticPaths = (async ({ paginate }) => {
|
||||||
|
return await getStaticPathsBlogCategory({ paginate });
|
||||||
|
}) satisfies GetStaticPaths;
|
||||||
|
|
||||||
|
type Props = InferGetStaticPropsType<typeof getStaticPaths> & { category: Record<string, string> };
|
||||||
|
|
||||||
|
const { page, category } = Astro.props as Props;
|
||||||
|
|
||||||
|
const currentPage = page.currentPage ?? 1;
|
||||||
|
|
||||||
|
const metadata = {
|
||||||
|
title: `Category '${category.title}' ${currentPage > 1 ? ` — Page ${currentPage}` : ''}`,
|
||||||
|
robots: {
|
||||||
|
index: blogCategoryRobots?.index,
|
||||||
|
follow: blogCategoryRobots?.follow,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout metadata={metadata}>
|
||||||
|
<section class="px-4 md:px-6 py-12 sm:py-16 lg:py-20 mx-auto max-w-4xl">
|
||||||
|
<Headline>{category.title}</Headline>
|
||||||
|
<BlogList posts={page.data} />
|
||||||
|
<Pagination prevUrl={page.url.prev} nextUrl={page.url.next} />
|
||||||
|
</section>
|
||||||
|
</Layout>
|
37
v3/src/pages/[...blog]/[tag]/[...page].astro
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
import type { InferGetStaticPropsType, GetStaticPaths } from 'astro';
|
||||||
|
import { blogTagRobots, getStaticPathsBlogTag } from '~/utils/blog';
|
||||||
|
|
||||||
|
import Layout from '~/layouts/PageLayout.astro';
|
||||||
|
import BlogList from '~/components/blog/List.astro';
|
||||||
|
import Headline from '~/components/blog/Headline.astro';
|
||||||
|
import Pagination from '~/components/blog/Pagination.astro';
|
||||||
|
|
||||||
|
export const prerender = true;
|
||||||
|
|
||||||
|
export const getStaticPaths = (async ({ paginate }) => {
|
||||||
|
return await getStaticPathsBlogTag({ paginate });
|
||||||
|
}) satisfies GetStaticPaths;
|
||||||
|
|
||||||
|
type Props = InferGetStaticPropsType<typeof getStaticPaths>;
|
||||||
|
|
||||||
|
const { page, tag } = Astro.props as Props;
|
||||||
|
|
||||||
|
const currentPage = page.currentPage ?? 1;
|
||||||
|
|
||||||
|
const metadata = {
|
||||||
|
title: `Posts by tag '${tag.title}'${currentPage > 1 ? ` — Page ${currentPage} ` : ''}`,
|
||||||
|
robots: {
|
||||||
|
index: blogTagRobots?.index,
|
||||||
|
follow: blogTagRobots?.follow,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
---
|
||||||
|
|
||||||
|
<Layout metadata={metadata}>
|
||||||
|
<section class="px-4 md:px-6 py-12 sm:py-16 lg:py-20 mx-auto max-w-4xl">
|
||||||
|
<Headline>Tag: {tag.title}</Headline>
|
||||||
|
<BlogList posts={page.data} />
|
||||||
|
<Pagination prevUrl={page.url.prev} nextUrl={page.url.next} />
|
||||||
|
</section>
|
||||||
|
</Layout>
|