Merge branch 'main' of https://github.com/widgeter/astrowind
@ -12,7 +12,7 @@
|
||||
- ✅ Integration with **Tailwind CSS** ([@astrojs/tailwind](https://docs.astro.build/en/guides/integrations-guide/tailwind/)) supporting **Dark mode** and ***RTL***.
|
||||
- ✅ **Production-ready** scores in [Lighthouse](https://web.dev/measure/) and [PageSpeed Insights](https://pagespeed.web.dev/) reports.
|
||||
- ✅ **Fast and SEO friendly blog** with automatic **RSS feed** ([@astrojs/rss](https://docs.astro.build/en/guides/rss/)), [**MDX** support](https://docs.astro.build/en/guides/integrations-guide/mdx/), **Categories & Tags**, **Social Share**, ...
|
||||
- ✅ **Image optimization** ([@astrojs/images](https://docs.astro.build/en/guides/integrations-guide/image/)) and **Font optimization**.
|
||||
- ✅ **Image Optimization** (using new [Astro Assets](https://astro.build/blog/images/) and [Unpic](https://unpic.pics/lib/) for Universal image CDN) and **Font optimization**.
|
||||
- ✅ Generation of **project sitemap** based on your routes ([@astrojs/sitemap](https://docs.astro.build/en/guides/integrations-guide/sitemap/)).
|
||||
- ✅ **Open Graph tags** for social media sharing.
|
||||
- ✅ **Analytics** built-in Google Analytics, and Splitbee integration.
|
||||
|
@ -5,10 +5,10 @@ import { defineConfig } from 'astro/config';
|
||||
|
||||
import tailwind from '@astrojs/tailwind';
|
||||
import sitemap from '@astrojs/sitemap';
|
||||
import image from '@astrojs/image';
|
||||
import mdx from '@astrojs/mdx';
|
||||
import icon from 'astro-icon';
|
||||
import partytown from '@astrojs/partytown';
|
||||
import tasks from "./src/utils/tasks"
|
||||
|
||||
import { readingTimeRemarkPlugin } from './src/utils/frontmatter.mjs';
|
||||
|
||||
@ -36,9 +36,7 @@ export default defineConfig({
|
||||
applyBaseStyles: false,
|
||||
}),
|
||||
sitemap(),
|
||||
image({
|
||||
serviceEntryPoint: '@astrojs/image/sharp',
|
||||
}),
|
||||
|
||||
mdx(),
|
||||
icon({
|
||||
include: {
|
||||
@ -63,12 +61,18 @@ export default defineConfig({
|
||||
config: { forward: ['dataLayer.push'] },
|
||||
})
|
||||
),
|
||||
|
||||
tasks()
|
||||
],
|
||||
|
||||
markdown: {
|
||||
remarkPlugins: [readingTimeRemarkPlugin],
|
||||
},
|
||||
|
||||
experimental:{
|
||||
assets: true
|
||||
},
|
||||
|
||||
vite: {
|
||||
resolve: {
|
||||
alias: {
|
||||
|
@ -13,7 +13,6 @@
|
||||
"lint:eslint": "eslint . --ext .js,.ts,.astro"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/image": "^0.17.3",
|
||||
"@astrojs/mdx": "^0.19.7",
|
||||
"@astrojs/partytown": "^1.2.3",
|
||||
"@astrojs/rss": "^2.4.4",
|
||||
@ -29,9 +28,9 @@
|
||||
"@types/lodash.merge": "^4.6.7",
|
||||
"@typescript-eslint/eslint-plugin": "^6.3.0",
|
||||
"@typescript-eslint/parser": "^6.3.0",
|
||||
"astro": "^2.10.5",
|
||||
"astro": "^2.10.7",
|
||||
"astro-icon": "^1.0.0-next.2",
|
||||
"eslint": "^8.46.0",
|
||||
"eslint": "^8.47.0",
|
||||
"eslint-plugin-astro": "^0.28.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
@ -41,11 +40,11 @@
|
||||
"prettier": "^3.0.1",
|
||||
"prettier-plugin-astro": "^0.11.0",
|
||||
"reading-time": "^1.5.0",
|
||||
"sharp": "^0.32.4",
|
||||
"svgo": "3.0.2",
|
||||
"tailwind-merge": "^1.14.0",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"typescript": "^5.1.6"
|
||||
"typescript": "^5.1.6",
|
||||
"unpic": "^3.10.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.12.0"
|
||||
|
BIN
src/assets/favicons/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 276 KiB |
Before Width: | Height: | Size: 136 KiB |
Before Width: | Height: | Size: 108 KiB |
Before Width: | Height: | Size: 91 KiB |
Before Width: | Height: | Size: 49 KiB |
@ -29,7 +29,7 @@
|
||||
}
|
||||
|
||||
.btn {
|
||||
@apply inline-flex items-center justify-center rounded-full shadow-md 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;
|
||||
@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;
|
||||
}
|
||||
|
||||
.btn-ghost {
|
||||
|
@ -1,9 +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} />
|
||||
|
@ -1,19 +1,19 @@
|
||||
---
|
||||
import { Picture } from '@astrojs/image/components';
|
||||
import type { ImageMetadata } from 'astro';
|
||||
|
||||
import { APP_BLOG_CONFIG } from '~/utils/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)) as ImageMetadata | undefined;
|
||||
const image = (await findImage(post.image));
|
||||
---
|
||||
|
||||
<article class="mb-6 transition">
|
||||
@ -21,15 +21,15 @@ const image = (await findImage(post.image)) as ImageMetadata | undefined;
|
||||
{
|
||||
image && (
|
||||
<a href={getPermalink(post.permalink, 'post')}>
|
||||
<Picture
|
||||
<Image
|
||||
src={image}
|
||||
class="md:object-cover w-full md:w-auto md:h-full rounded shadow-lg bg-gray-400 dark:bg-slate-700"
|
||||
class="w-full md:h-full rounded shadow-lg bg-gray-400 dark:bg-slate-700"
|
||||
widths={[400, 900]}
|
||||
width={400}
|
||||
height={224}
|
||||
sizes="(max-width: 900px) 400px, 900px"
|
||||
alt={post.title}
|
||||
aspectRatio="16:9"
|
||||
layout="cover"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
|
@ -1,7 +1,7 @@
|
||||
---
|
||||
import { Picture } from '@astrojs/image/components';
|
||||
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_CONFIG } from '~/utils/config';
|
||||
@ -27,10 +27,11 @@ const link = APP_BLOG_CONFIG?.post?.isEnabled ? getPermalink(post.permalink, 'po
|
||||
<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 && (
|
||||
<Picture
|
||||
<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"
|
||||
|
@ -1,7 +1,7 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import { Picture } from '@astrojs/image/components';
|
||||
|
||||
import Image from '~/components/common/Image.astro';
|
||||
import PostTags from '~/components/blog/Tags.astro';
|
||||
import SocialShare from '~/components/common/SocialShare.astro';
|
||||
|
||||
@ -53,19 +53,17 @@ const Content = post?.Content || null;
|
||||
|
||||
{
|
||||
post.image ? (
|
||||
<Picture
|
||||
<Image
|
||||
src={post.image}
|
||||
class="max-w-full lg:max-w-6xl mx-auto mb-6 sm:rounded-md bg-gray-400 dark:bg-slate-700"
|
||||
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 || ''}
|
||||
loading="eager"
|
||||
aspectRatio={16 / 9}
|
||||
width={900}
|
||||
height={506}
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
background={undefined}
|
||||
/>
|
||||
) : (
|
||||
<div class="max-w-3xl mx-auto px-4 sm:px-6">
|
||||
|
52
src/components/common/Image.astro
Normal file
@ -0,0 +1,52 @@
|
||||
---
|
||||
import { findImage } from '~/utils/images';
|
||||
import {
|
||||
getImagesOptimized,
|
||||
astroAsseetsOptimizer,
|
||||
unpicOptimizer,
|
||||
type ImageProps,
|
||||
type AttributesProps
|
||||
} from '~/utils/images-optimization';
|
||||
|
||||
type Props = ImageProps;
|
||||
type ImageType = {
|
||||
src: string;
|
||||
attributes: AttributesProps;
|
||||
}
|
||||
|
||||
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 (_image !== null && typeof _image === 'object') {
|
||||
image = await getImagesOptimized(_image, props, astroAsseetsOptimizer);
|
||||
} else if (typeof _image === 'string' && (_image.startsWith('http://') || _image.startsWith('https://'))) {
|
||||
image = await getImagesOptimized(_image, props, unpicOptimizer);
|
||||
} else if (_image) {
|
||||
image = await getImagesOptimized(_image, props);
|
||||
}
|
||||
---
|
||||
|
||||
{!image ? <Fragment /> : <img src={image.src} {...image.attributes} />}
|
@ -1,9 +1,9 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import { Picture } from '@astrojs/image/components';
|
||||
import type { Content } from '~/types';
|
||||
import Headline from '../ui/Headline.astro';
|
||||
import WidgetWrapper from '../ui/WidgetWrapper.astro';
|
||||
import Image from '~/components/common/Image.astro';
|
||||
|
||||
const {
|
||||
title = await Astro.slots.render('title'),
|
||||
@ -70,13 +70,13 @@ const {
|
||||
{typeof image === 'string' ? (
|
||||
<Fragment set:html={image} />
|
||||
) : (
|
||||
<Picture
|
||||
<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"
|
||||
aspectRatio="500:500"
|
||||
layout="responsive"
|
||||
{...(image as any)}
|
||||
/>
|
||||
)}
|
||||
|
@ -1,8 +1,8 @@
|
||||
---
|
||||
import { Picture } from '@astrojs/image/components';
|
||||
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 } from '~/types';
|
||||
|
||||
const {
|
||||
@ -39,12 +39,12 @@ const {
|
||||
{typeof image === 'string' ? (
|
||||
<Fragment set:html={image} />
|
||||
) : (
|
||||
<Picture
|
||||
<Image
|
||||
class="w-full h-80 object-cover rounded-xl mx-auto bg-gray-500 shadow-lg"
|
||||
width={0}
|
||||
width="auto"
|
||||
height={320}
|
||||
widths={[400, 768]}
|
||||
aspectRatio="16:7"
|
||||
layout="fullWidth"
|
||||
{...(image as any)}
|
||||
/>
|
||||
)}
|
||||
|
@ -1,6 +1,6 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import { Picture } from '@astrojs/image/components';
|
||||
import Image from '~/components/common/Image.astro';
|
||||
import WidgetWrapper from '../ui/WidgetWrapper.astro';
|
||||
|
||||
const {
|
||||
@ -88,11 +88,10 @@ const {
|
||||
{typeof image === 'string' ? (
|
||||
<Fragment set:html={image} />
|
||||
) : (
|
||||
<Picture
|
||||
<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"
|
||||
aspectRatio={1024 / 576}
|
||||
loading="eager"
|
||||
width={1024}
|
||||
height={576}
|
||||
|
@ -1,6 +1,6 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import { Picture } from '@astrojs/image/components';
|
||||
import Image from '~/components/common/Image.astro';
|
||||
import { CallToAction } from '~/types';
|
||||
|
||||
export interface Props {
|
||||
@ -88,11 +88,10 @@ const {
|
||||
{typeof image === 'string' ? (
|
||||
<Fragment set:html={image} />
|
||||
) : (
|
||||
<Picture
|
||||
<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"
|
||||
aspectRatio={600 / 600}
|
||||
loading="eager"
|
||||
width={600}
|
||||
height={600}
|
||||
|
@ -35,9 +35,9 @@ const {
|
||||
</div>
|
||||
)}
|
||||
{title && (
|
||||
<h6 class="text-sm font-medium uppercase tracking-widest text-gray-800 dark:text-slate-400 lg:text-base">
|
||||
<div class="text-sm font-medium uppercase tracking-widest text-gray-800 dark:text-slate-400 lg:text-base">
|
||||
{title}
|
||||
</h6>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
|
@ -1,8 +1,8 @@
|
||||
---
|
||||
import { Picture } from '@astrojs/image/components';
|
||||
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 } from "~/types";
|
||||
|
||||
const {
|
||||
@ -37,13 +37,13 @@ const {
|
||||
(typeof image === 'string' ? (
|
||||
<Fragment set:html={image} />
|
||||
) : (
|
||||
<Picture
|
||||
<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"
|
||||
aspectRatio="432:768"
|
||||
width={432}
|
||||
height={768}
|
||||
layout="cover"
|
||||
src={image?.src}
|
||||
alt={image?.alt || ""}
|
||||
/>
|
||||
|
@ -1,9 +1,10 @@
|
||||
---
|
||||
import Headline from '~/components/ui/Headline.astro';
|
||||
import WidgetWrapper from '~/components/ui/WidgetWrapper.astro';
|
||||
import CTA from '~/components/ui/CTA.astro';
|
||||
import Image from '~/components/common/Image.astro';
|
||||
import type { Testimonials } from '~/types';
|
||||
import CTA from '../ui/CTA.astro';
|
||||
import { Picture } from '@astrojs/image/components';
|
||||
|
||||
|
||||
const {
|
||||
title = '',
|
||||
@ -43,12 +44,12 @@ const {
|
||||
{typeof image === 'string' ? (
|
||||
<Fragment set:html={image} />
|
||||
) : (
|
||||
<Picture
|
||||
<Image
|
||||
class="h-10 w-10 rounded-full border border-slate-200 dark:border-slate-600"
|
||||
width={40}
|
||||
height={40}
|
||||
widths={[400, 768]}
|
||||
aspectRatio="1:1"
|
||||
layout="fixed"
|
||||
{...(image as any)}
|
||||
/>
|
||||
)}
|
||||
|
@ -2,7 +2,7 @@
|
||||
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: ~/assets/images/stickers.jpg
|
||||
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
|
||||
|
@ -2,7 +2,7 @@
|
||||
publishDate: 2023-01-12T00:00:00Z
|
||||
title: Get started with AstroWind to create a website using Astro and Tailwind CSS
|
||||
excerpt: Sint sit cillum pariatur eiusmod nulla pariatur ipsum. Sit laborum anim qui mollit tempor pariatur.
|
||||
image: ~/assets/images/do-more.jpg
|
||||
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
|
||||
|
@ -2,7 +2,7 @@
|
||||
publishDate: 2023-01-06T00:00:00Z
|
||||
title: How to customize AstroWind template to suit your branding
|
||||
excerpt: Sint sit cillum pariatur eiusmod nulla pariatur ipsum. Sit laborum anim qui mollit tempor pariatur.
|
||||
image: ~/assets/images/colors.jpg
|
||||
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
|
||||
|
@ -2,7 +2,7 @@
|
||||
publishDate: 2023-01-09T00:00:00Z
|
||||
title: Useful tools and resources to create a professional website
|
||||
excerpt: Sint sit cillum pariatur eiusmod nulla pariatur ipsum. Sit laborum anim qui mollit tempor pariatur.
|
||||
image: ~/assets/images/tools.jpg
|
||||
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
|
||||
|
2
src/env.d.ts
vendored
@ -1,3 +1,3 @@
|
||||
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
/// <reference types="@astrojs/image/client" />
|
||||
/// <reference types="astro/client-image" />
|
||||
|
@ -7,7 +7,9 @@ import Pagination from '~/components/blog/Pagination.astro';
|
||||
|
||||
import { blogListRobots, getStaticPathsBlogList } from '~/utils/blog';
|
||||
|
||||
export const getStaticPaths = getStaticPathsBlogList();
|
||||
export async function getStaticPaths ({ paginate }) {
|
||||
return await getStaticPathsBlogList({ paginate });
|
||||
}
|
||||
|
||||
const { page } = Astro.props;
|
||||
const currentPage = page.currentPage ?? 1;
|
||||
|
@ -6,7 +6,9 @@ import BlogList from '~/components/blog/List.astro';
|
||||
import Headline from '~/components/blog/Headline.astro';
|
||||
import Pagination from '~/components/blog/Pagination.astro';
|
||||
|
||||
export const getStaticPaths = getStaticPathsBlogCategory();
|
||||
export async function getStaticPaths ({ paginate }) {
|
||||
return await getStaticPathsBlogCategory({ paginate });
|
||||
}
|
||||
|
||||
const { page, category } = Astro.props;
|
||||
|
||||
|
@ -6,7 +6,9 @@ import BlogList from '~/components/blog/List.astro';
|
||||
import Headline from '~/components/blog/Headline.astro';
|
||||
import Pagination from '~/components/blog/Pagination.astro';
|
||||
|
||||
export const getStaticPaths = getStaticPathsBlogTag();
|
||||
export async function getStaticPaths ({ paginate }) {
|
||||
return await getStaticPathsBlogTag({ paginate });
|
||||
}
|
||||
|
||||
const { page, tag } = Astro.props;
|
||||
|
||||
|
@ -9,7 +9,9 @@ import { getCanonical, getPermalink } from '~/utils/permalinks';
|
||||
import { getStaticPathsBlogPost, blogPostRobots } from '~/utils/blog';
|
||||
import { findImage } from '~/utils/images';
|
||||
|
||||
export const getStaticPaths = getStaticPathsBlogPost();
|
||||
export async function getStaticPaths () {
|
||||
return await getStaticPathsBlogPost();
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
|
||||
@ -26,7 +28,7 @@ const metadata = merge(
|
||||
},
|
||||
openGraph: {
|
||||
type: 'article',
|
||||
...(image ? { images: [{ url: image?.src, width: image?.width, height: image?.height }] } : {}),
|
||||
...(image ? { images: [{ url: image, width: image?.width, height: image?.height }] } : {}),
|
||||
},
|
||||
},
|
||||
{ ...(post?.metadata ? { ...post.metadata, canonical: post.metadata?.canonical || url } : {}) }
|
||||
|
@ -14,7 +14,7 @@ const metadata = {
|
||||
<Layout metadata={metadata}>
|
||||
<!-- Hero Widget ******************* -->
|
||||
|
||||
<Hero image={{ src: import('~/assets/images/caos.jpg'), alt: 'Caos Image' }}>
|
||||
<Hero image={{ src: 'https://images.unsplash.com/photo-1559136555-9303baea8ebd?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80', alt: 'Caos Image' }}>
|
||||
<Fragment slot="title">
|
||||
Elevate your online presence with our <br />
|
||||
<span class="text-accent dark:text-white highlight"> Beautiful Website Templates</span>
|
||||
@ -112,7 +112,7 @@ const metadata = {
|
||||
},
|
||||
]}
|
||||
image={{
|
||||
src: import('~/assets/images/colors.jpg'),
|
||||
src: '~/assets/images/colors.jpg',
|
||||
alt: 'Colorful Image',
|
||||
}}
|
||||
/>
|
||||
|
@ -24,7 +24,7 @@ const metadata = {
|
||||
<Hero
|
||||
callToAction={{ text: 'Get template', href: 'https://github.com/onwidget/astrowind', icon: 'tabler:download' }}
|
||||
callToAction2={{ text: 'Learn more', href: '#features' }}
|
||||
image={{ src: import('~/assets/images/hero.png'), alt: 'AstroWind Hero Image' }}
|
||||
image={{ src: '~/assets/images/hero.png', alt: 'AstroWind Hero Image' }}
|
||||
>
|
||||
<Fragment slot="title">
|
||||
Free template for <span class="hidden xl:inline">creating websites with</span>
|
||||
@ -96,8 +96,9 @@ const metadata = {
|
||||
<!-- Content Widget **************** -->
|
||||
|
||||
<Content
|
||||
isReversed
|
||||
tagline="Inside template"
|
||||
title="And what's inside? ..."
|
||||
title="AstroWind's Blueprint: Fun Meets Functionality!"
|
||||
items={[
|
||||
{
|
||||
title: 'Per ei quaeque sensibus',
|
||||
@ -116,7 +117,7 @@ const metadata = {
|
||||
},
|
||||
]}
|
||||
image={{
|
||||
src: import('~/assets/images/caos.jpg'),
|
||||
src: 'https://images.unsplash.com/photo-1519389950473-47ba0277781c?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80',
|
||||
alt: 'Colorful Image',
|
||||
}}
|
||||
>
|
||||
@ -133,6 +134,43 @@ const metadata = {
|
||||
|
||||
<!-- Content Widget **************** -->
|
||||
|
||||
<Content
|
||||
isAfterContent
|
||||
items={[
|
||||
{
|
||||
title: 'Per ei quaeque sensibus',
|
||||
description:
|
||||
'Ex usu illum iudico molestie. Pro ne agam facete mediocritatem, ridens labore facete mea ei. Pro id apeirian dignissim.',
|
||||
},
|
||||
{
|
||||
title: 'Cu imperdiet posidonium sed',
|
||||
description:
|
||||
'Amet utinam aliquando ut mea, malis admodum ocurreret nec et, elit tibique cu nec. Nec ex maluisset inciderint, ex quis.',
|
||||
},
|
||||
{
|
||||
title: 'Nulla omittam sadipscing mel ne',
|
||||
description:
|
||||
'At sed possim oporteat probatus, justo graece ne nec, minim commodo legimus ut vix. Ut eos iudico quando soleat, nam modus.',
|
||||
},
|
||||
]}
|
||||
image={{
|
||||
src: 'https://images.unsplash.com/photo-1600132806370-bf17e65e942f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2194&q=80',
|
||||
alt: 'Blueprint Image',
|
||||
}}
|
||||
>
|
||||
<Fragment slot="content">
|
||||
<h3 class="text-2xl font-bold tracking-tight dark:text-white sm:text-3xl mb-2">Ad vix debet docendi</h3>
|
||||
Ne dicta praesent ocurreret has, diam theophrastus at pro. Eos etiam regione ut, persius eripuit quo id. Sit te
|
||||
euismod tacimates.
|
||||
</Fragment>
|
||||
|
||||
<Fragment slot="bg">
|
||||
<div class="absolute inset-0 bg-blue-50 dark:bg-transparent"></div>
|
||||
</Fragment>
|
||||
</Content>
|
||||
|
||||
<!-- Content Widget **************** -->
|
||||
|
||||
<Content
|
||||
isReversed
|
||||
isAfterContent
|
||||
@ -157,8 +195,8 @@ const metadata = {
|
||||
},
|
||||
]}
|
||||
image={{
|
||||
src: import('~/assets/images/vintage.jpg'),
|
||||
alt: 'Vintage Image',
|
||||
src: 'https://images.unsplash.com/photo-1611462985358-60d3498e0364?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80',
|
||||
alt: 'Astronauts Image',
|
||||
}}
|
||||
>
|
||||
<Fragment slot="bg">
|
||||
@ -174,19 +212,19 @@ const metadata = {
|
||||
{
|
||||
title: 'Step 1: <span class="font-medium">Download</span>',
|
||||
description:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi sagittis, quam nec venenatis lobortis, mirisus tempus nulla, sed porttitor est nibh at nulla. Praesent placerat enim ut ex tincidunt vehicula.',
|
||||
'Kickstart with GitHub! Either fork the AstroWind template or simply click \'Use this template\'. Your canvas awaits, ready for your digital masterpiece. In just a few clicks, you\'ve already set the foundation.',
|
||||
icon: 'tabler:package',
|
||||
},
|
||||
{
|
||||
title: 'Step 2: <span class="font-medium">Add content</em>',
|
||||
description:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi sagittis, quam nec venenatis lobortis, mirisus tempus nulla, sed porttitor est nibh at nulla.',
|
||||
'Pour your vision into it. Add images, text, and all that jazz to breathe life into your digital space. Remember, it\'s the content that tells your story, so make it captivating.',
|
||||
icon: 'tabler:letter-case',
|
||||
},
|
||||
{
|
||||
title: 'Step 3: <span class="font-medium">Customize styles</span>',
|
||||
description:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi sagittis, quam nec venenatis lobortis, mirisus tempus nulla, sed porttitor est nibh at nulla. Praesent placerat enim ut ex tincidunt vehicula. Fusce sit amet dui tellus.',
|
||||
'Give it your personal touch. Tailor colors, fonts, and layouts until it feels just right. Your unique flair, amplified by AstroWind! Precision in design ensures a seamless user experience.',
|
||||
icon: 'tabler:paint',
|
||||
},
|
||||
{
|
||||
@ -195,7 +233,7 @@ const metadata = {
|
||||
},
|
||||
]}
|
||||
image={{
|
||||
src: import('~/assets/images/creativity.jpg'),
|
||||
src: 'https://images.unsplash.com/photo-1616198814651-e71f960c3180?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=987&q=80',
|
||||
alt: 'Steps image',
|
||||
}}
|
||||
/>
|
||||
@ -210,55 +248,55 @@ const metadata = {
|
||||
{
|
||||
title: 'Headers',
|
||||
description:
|
||||
'In general, Headers contain information that makes it easier for visitors to interact with the website.',
|
||||
'Ever tried driving without GPS? Boom! That\'s why websites need headers for direction.',
|
||||
icon: 'flat-color-icons:template',
|
||||
},
|
||||
{
|
||||
title: 'Heros',
|
||||
description:
|
||||
'If you want your website to get more than its fair share of visitors, the Hero section needs to be stellar.',
|
||||
'Picture a superhero landing – epic, right? That\'s the job of a Hero section, making grand entrances!',
|
||||
icon: 'flat-color-icons:gallery',
|
||||
},
|
||||
{
|
||||
title: 'Features',
|
||||
description:
|
||||
'Display your product in action and how the Features actually create a solution for your target customer.',
|
||||
'Where websites strut their stuff and show off superpowers. No holding back on the bragging rights here!',
|
||||
icon: 'flat-color-icons:approval',
|
||||
},
|
||||
{
|
||||
title: 'Content',
|
||||
description:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore.',
|
||||
'Dive into the meat and potatoes of a site; without it, you\'d just be window shopping. Content is king.',
|
||||
icon: 'flat-color-icons:document',
|
||||
},
|
||||
{
|
||||
title: 'Call-to-Action',
|
||||
description:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore.',
|
||||
'That enthusiastic friend who\'s always urging, "Do it! Do it!"? Yeah, that\'s this button nudging you towards adventure.',
|
||||
icon: 'flat-color-icons:advertising',
|
||||
},
|
||||
{
|
||||
title: 'Pricing',
|
||||
description:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore.',
|
||||
'Behold the dessert menu of the website world. Tempting choices await, can you resist?',
|
||||
icon: 'flat-color-icons:currency-exchange',
|
||||
},
|
||||
{
|
||||
title: 'Testimonial',
|
||||
description:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore.',
|
||||
'Step into the gossip corner! Here, other visitors spill the beans and share the juicy details.',
|
||||
icon: 'flat-color-icons:voice-presentation',
|
||||
},
|
||||
{
|
||||
title: 'Contact',
|
||||
description:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore.',
|
||||
'Like a digital mailbox, but faster! Drop a line, ask a question, or send a virtual high-five. Ding! Message in.',
|
||||
icon: 'flat-color-icons:business-contact',
|
||||
},
|
||||
{
|
||||
title: 'Footers',
|
||||
description:
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore.',
|
||||
'The footer\'s like the credits of a movie but sprinkled with easter eggs. Time to hunt!',
|
||||
icon: 'flat-color-icons:database',
|
||||
},
|
||||
]}
|
||||
@ -285,6 +323,11 @@ const metadata = {
|
||||
subtitle="Duis turpis dui, fringilla mattis sem nec, fringilla euismod neque. Morbi tincidunt lacus nec tortor scelerisque pulvinar."
|
||||
tagline="FAQs"
|
||||
items={[
|
||||
{
|
||||
title: "Why AstroWind?",
|
||||
description:
|
||||
"Michael Knight a young loner on a crusade to champion the cause of the innocent. The helpless. The powerless in a world of criminals who operate above the law. Here he comes Here comes Speed Racer. He's a demon on wheels.",
|
||||
},
|
||||
{
|
||||
title: 'What do I need to start?',
|
||||
description:
|
||||
@ -300,11 +343,6 @@ const metadata = {
|
||||
description:
|
||||
"A flower in my garden, a mystery in my panties. Heart attack never stopped old Big Bear. I didn't even know we were calling him Big Bear.",
|
||||
},
|
||||
{
|
||||
title: "What's an example of when you changed your mind?",
|
||||
description:
|
||||
"Michael Knight a young loner on a crusade to champion the cause of the innocent. The helpless. The powerless in a world of criminals who operate above the law. Here he comes Here comes Speed Racer. He's a demon on wheels.",
|
||||
},
|
||||
{
|
||||
title: 'What is something that you would like to try again?',
|
||||
description:
|
||||
|
@ -40,7 +40,7 @@ const metadata = {
|
||||
<Hero2
|
||||
callToAction={{ text: 'Download App', href: 'https://github.com/onwidget/astrowind', icon: 'tabler:download' }}
|
||||
callToAction2={{ text: 'Learn more', href: '#features' }}
|
||||
image={{ src: import('~/assets/images/hero.png'), alt: 'AstroWind Hero Image' }}
|
||||
image={{ src: 'https://images.unsplash.com/photo-1535303311164-664fc9ec6532?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=987&q=80', alt: 'AstroWind Hero Image' }}
|
||||
>
|
||||
<Fragment slot="title">
|
||||
Free template for <span class="hidden lg:inline">create your website <br />with</span>
|
||||
@ -91,7 +91,7 @@ const metadata = {
|
||||
},
|
||||
]}
|
||||
image={{
|
||||
src: import('~/assets/images/colors.jpg'),
|
||||
src: 'https://images.unsplash.com/photo-1521517407911-565264e7d82d?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2069&q=80',
|
||||
alt: 'Colorful Image',
|
||||
}}
|
||||
/>
|
||||
@ -99,6 +99,7 @@ const metadata = {
|
||||
<!-- Content Widget **************** -->
|
||||
|
||||
<Content
|
||||
isReversed
|
||||
items={[
|
||||
{
|
||||
title: 'High-Quality Designs',
|
||||
@ -128,7 +129,7 @@ const metadata = {
|
||||
},
|
||||
]}
|
||||
image={{
|
||||
src: import('~/assets/images/caos.jpg'),
|
||||
src: 'https://images.unsplash.com/photo-1576153192621-7a3be10b356e?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1674&q=80',
|
||||
alt: 'Colorful Image',
|
||||
}}
|
||||
>
|
||||
@ -140,7 +141,6 @@ const metadata = {
|
||||
<!-- Content Widget **************** -->
|
||||
|
||||
<Content
|
||||
isReversed
|
||||
isAfterContent
|
||||
items={[
|
||||
{
|
||||
@ -171,7 +171,7 @@ const metadata = {
|
||||
},
|
||||
]}
|
||||
image={{
|
||||
src: import('~/assets/images/vintage.jpg'),
|
||||
src: 'https://images.unsplash.com/photo-1453738773917-9c3eff1db985?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80',
|
||||
alt: 'Vintage Image',
|
||||
}}
|
||||
>
|
||||
@ -202,7 +202,7 @@ const metadata = {
|
||||
name: 'Cary Kennedy',
|
||||
job: 'Film director',
|
||||
image: {
|
||||
src: import('~/assets/images/colors.jpg'),
|
||||
src: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80',
|
||||
alt: 'Cary Kennedy Image',
|
||||
},
|
||||
},
|
||||
@ -212,7 +212,7 @@ const metadata = {
|
||||
name: 'Josh Wilkinson',
|
||||
job: 'Product Manager',
|
||||
image: {
|
||||
src: import('~/assets/images/vintage.jpg'),
|
||||
src: 'https://images.unsplash.com/flagged/photo-1570612861542-284f4c12e75f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80',
|
||||
alt: 'Josh Wilkinson Image',
|
||||
},
|
||||
},
|
||||
@ -222,7 +222,7 @@ const metadata = {
|
||||
name: 'Sidney Hansen',
|
||||
job: 'Decorator',
|
||||
image: {
|
||||
src: import('~/assets/images/caos.jpg'),
|
||||
src: 'https://images.unsplash.com/photo-1512361436605-a484bdb34b5f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80',
|
||||
alt: 'Sidney Hansen Image',
|
||||
},
|
||||
},
|
||||
|
@ -3,6 +3,7 @@ import Layout from '~/layouts/PageLayout.astro';
|
||||
|
||||
import Header from '~/components/widgets/Header.astro';
|
||||
import Hero2 from '~/components/widgets/Hero2.astro';
|
||||
import Features from '~/components/widgets/Features.astro';
|
||||
import Steps2 from '~/components/widgets/Steps2.astro';
|
||||
import Content from '~/components/widgets/Content.astro';
|
||||
import CallToAction from '~/components/widgets/CallToAction.astro';
|
||||
@ -39,7 +40,7 @@ const metadata = {
|
||||
<Hero2
|
||||
callToAction={{ text: 'Get template', href: 'https://github.com/onwidget/astrowind', icon: 'tabler:download' }}
|
||||
callToAction2={{ text: 'Learn more', href: '#features' }}
|
||||
image={{ src: import('~/assets/images/hero.png'), alt: 'AstroWind Hero Image' }}
|
||||
image={{ src: 'https://images.unsplash.com/photo-1580481072645-022f9a6dbf27?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80', alt: 'AstroWind Hero Image' }}
|
||||
>
|
||||
<Fragment slot="title">
|
||||
Free template for <br />
|
||||
@ -56,6 +57,51 @@ const metadata = {
|
||||
</Fragment>
|
||||
</Hero2>
|
||||
|
||||
<!-- Features Widget *************** -->
|
||||
|
||||
<Features
|
||||
id="features"
|
||||
tagline="Features"
|
||||
title="Main features of our templates"
|
||||
subtitle="Possess several key characteristics to effectively cater to the needs of startups and entrepreneurs."
|
||||
columns={3}
|
||||
items={[
|
||||
{
|
||||
title: 'Modern and Professional Design',
|
||||
description:
|
||||
'Have a contemporary design that reflects current design trends and gives a professional impression.',
|
||||
icon: 'tabler:artboard',
|
||||
},
|
||||
{
|
||||
title: 'Responsive and Mobile-Friendly',
|
||||
description: 'Adapt seamlessly to different screen sizes and devices to ensure a consistent experience.',
|
||||
icon: 'tabler:picture-in-picture',
|
||||
},
|
||||
{
|
||||
title: 'Customizability',
|
||||
description:
|
||||
'Easily customizable, allowing users to adapt the design, colors, typography, and content to match their brand identity.',
|
||||
icon: 'tabler:adjustments-horizontal',
|
||||
},
|
||||
{
|
||||
title: 'Fast Loading Times',
|
||||
description: 'Optimized for speed to ensure a smooth user experience and favorable search engine rankings.',
|
||||
icon: 'tabler:rocket',
|
||||
},
|
||||
{
|
||||
title: 'Search Engine Optimization (SEO)',
|
||||
description:
|
||||
'Incorporate SEO best practices in template structure and code to improve visibility in search engine results.',
|
||||
icon: 'tabler:arrows-right-left',
|
||||
},
|
||||
{
|
||||
title: 'Compatibility',
|
||||
description: 'The templates work seamlessly across various content management systems and website builders.',
|
||||
icon: 'tabler:plug-connected',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<!-- Content Widget **************** -->
|
||||
|
||||
<Content
|
||||
@ -78,7 +124,7 @@ const metadata = {
|
||||
},
|
||||
]}
|
||||
image={{
|
||||
src: import('~/assets/images/caos.jpg'),
|
||||
src: 'https://images.unsplash.com/photo-1620558138198-cfb9b4f3c294?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1671&q=80',
|
||||
alt: 'Colorful Image',
|
||||
}}
|
||||
>
|
||||
@ -115,7 +161,7 @@ const metadata = {
|
||||
},
|
||||
]}
|
||||
image={{
|
||||
src: import('~/assets/images/caos.jpg'),
|
||||
src: 'https://images.unsplash.com/photo-1531973486364-5fa64260d75b?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1658&q=80',
|
||||
alt: 'Colorful Image',
|
||||
}}
|
||||
>
|
||||
@ -153,7 +199,7 @@ const metadata = {
|
||||
},
|
||||
]}
|
||||
image={{
|
||||
src: import('~/assets/images/caos.jpg'),
|
||||
src: 'https://images.unsplash.com/photo-1635070041078-e363dbe005cb?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80',
|
||||
alt: 'Colorful Image',
|
||||
}}
|
||||
>
|
||||
|
@ -43,14 +43,18 @@ const metadata = {
|
||||
</Fragment>
|
||||
|
||||
<Fragment slot="image">
|
||||
<div class="relative h-0 pb-[56.25%]">
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
src="https://www.youtube.com/embed/dsTXcSeAZq8"
|
||||
title="YouTube video player"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture;"
|
||||
allowfullscreen
|
||||
style="width:100%"></iframe>
|
||||
class="absolute top-0 left-0 w-full h-full"
|
||||
>
|
||||
</iframe>
|
||||
</div>
|
||||
</Fragment>
|
||||
</Hero>
|
||||
|
||||
|
@ -178,7 +178,7 @@ const metadata = {
|
||||
},
|
||||
]}
|
||||
image={{
|
||||
src: import('~/assets/images/creativity.jpg'),
|
||||
src: 'https://images.unsplash.com/photo-1536816579748-4ecb3f03d72a?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=987&q=80',
|
||||
alt: 'Steps image',
|
||||
}}
|
||||
/>
|
||||
|
@ -16,7 +16,7 @@ export const get = async () => {
|
||||
|
||||
return rss({
|
||||
title: `${SITE_CONFIG.name}’s Blog`,
|
||||
description: METADATA_CONFIG?.description,
|
||||
description: METADATA_CONFIG?.description || "",
|
||||
site: import.meta.env.SITE,
|
||||
|
||||
items: posts.map((post) => ({
|
||||
|
@ -27,7 +27,7 @@ const metadata = {
|
||||
|
||||
<div
|
||||
slot="bg"
|
||||
class="absolute inset-0 bg-dark overflow-hidden brightness-[0.25] bg-cover bg-[url('~/assets/images/hero.png')]"
|
||||
class="absolute inset-0 bg-dark overflow-hidden brightness-[0.25] bg-cover bg-[url('https://images.unsplash.com/photo-1611462985358-60d3498e0364?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80')]"
|
||||
>
|
||||
</div>
|
||||
</Hero>
|
||||
@ -104,7 +104,7 @@ const metadata = {
|
||||
name: 'Emily Kennedy',
|
||||
job: 'Front-end developer',
|
||||
image: {
|
||||
src: import('~/assets/images/colors.jpg'),
|
||||
src: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80',
|
||||
alt: 'Emily Kennedy Image',
|
||||
},
|
||||
},
|
||||
@ -113,7 +113,7 @@ const metadata = {
|
||||
name: 'Sarah Hansen',
|
||||
job: 'Photographer',
|
||||
image: {
|
||||
src: import('~/assets/images/caos.jpg'),
|
||||
src: 'https://images.unsplash.com/flagged/photo-1570612861542-284f4c12e75f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80',
|
||||
alt: 'Sarah Hansen Image',
|
||||
},
|
||||
},
|
||||
@ -122,7 +122,7 @@ const metadata = {
|
||||
name: 'Mark Wilkinson',
|
||||
job: 'Small business owner',
|
||||
image: {
|
||||
src: import('~/assets/images/vintage.jpg'),
|
||||
src: 'https://images.unsplash.com/photo-1512361436605-a484bdb34b5f?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80',
|
||||
alt: 'Mark Wilkinson Image',
|
||||
},
|
||||
},
|
||||
|
@ -162,18 +162,16 @@ export const findLatestPosts = async ({ count }: { count?: number }): Promise<Ar
|
||||
};
|
||||
|
||||
/** */
|
||||
export const getStaticPathsBlogList =
|
||||
() =>
|
||||
async ({ paginate }) => {
|
||||
export const getStaticPathsBlogList = async ({ paginate }) => {
|
||||
if (!isBlogEnabled || !isBlogListRouteEnabled) return [];
|
||||
return paginate(await fetchPosts(), {
|
||||
params: { blog: BLOG_BASE || undefined },
|
||||
pageSize: blogPostsPerPage,
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
/** */
|
||||
export const getStaticPathsBlogPost = () => async () => {
|
||||
export const getStaticPathsBlogPost = async () => {
|
||||
if (!isBlogEnabled || !isBlogPostRouteEnabled) return [];
|
||||
return (await fetchPosts()).map((post) => ({
|
||||
params: {
|
||||
@ -184,9 +182,7 @@ export const getStaticPathsBlogPost = () => async () => {
|
||||
};
|
||||
|
||||
/** */
|
||||
export const getStaticPathsBlogCategory =
|
||||
() =>
|
||||
async ({ paginate }) => {
|
||||
export const getStaticPathsBlogCategory = async ({ paginate }) => {
|
||||
if (!isBlogEnabled || !isBlogCategoryRouteEnabled) return [];
|
||||
|
||||
const posts = await fetchPosts();
|
||||
@ -205,12 +201,10 @@ export const getStaticPathsBlogCategory =
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
/** */
|
||||
export const getStaticPathsBlogTag =
|
||||
() =>
|
||||
async ({ paginate }) => {
|
||||
export const getStaticPathsBlogTag = async ({ paginate }) => {
|
||||
if (!isBlogEnabled || !isBlogTagRouteEnabled) return [];
|
||||
|
||||
const posts = await fetchPosts();
|
||||
@ -229,4 +223,4 @@ export const getStaticPathsBlogTag =
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
};
|
||||
|
321
src/utils/images-optimization.ts
Normal file
@ -0,0 +1,321 @@
|
||||
import { getImage } from 'astro:assets';
|
||||
import { transformUrl, parseUrl } from 'unpic';
|
||||
|
||||
import type { ImageMetadata } from 'astro';
|
||||
import type { HTMLAttributes } from 'astro/types';
|
||||
|
||||
type Layout = 'fixed' | 'constrained' | 'fullWidth' | 'cover' | 'responsive' | 'contained';
|
||||
|
||||
export interface AttributesProps extends HTMLAttributes<'img'> {}
|
||||
|
||||
export interface ImageProps extends Omit<HTMLAttributes<'img'>, 'src'> {
|
||||
src?: string | ImageMetadata | null;
|
||||
width?: string | number | null;
|
||||
height?: string | number | null;
|
||||
alt?: string | null;
|
||||
loading?: 'eager' | 'lazy' | null;
|
||||
decoding?: 'sync' | 'async' | 'auto' | null;
|
||||
style?: string;
|
||||
srcset?: string | null;
|
||||
sizes?: string | null;
|
||||
fetchpriority?: 'high' | 'low' | 'auto' | null;
|
||||
|
||||
layout?: Layout;
|
||||
widths?: number[] | null;
|
||||
aspectRatio?: string | number | null;
|
||||
}
|
||||
|
||||
export type ImagesOptimizer = (
|
||||
image: ImageMetadata | string,
|
||||
breakpoints: number[],
|
||||
width?: number,
|
||||
height?: number
|
||||
) => Promise<Array<{ src: string; width: number }>>;
|
||||
|
||||
/* ******* */
|
||||
const config = {
|
||||
// FIXME: Use this when image.width is minor than deviceSizes
|
||||
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
|
||||
|
||||
deviceSizes: [
|
||||
640, // older and lower-end phones
|
||||
750, // iPhone 6-8
|
||||
828, // iPhone XR/11
|
||||
960, // older horizontal phones
|
||||
1080, // iPhone 6-8 Plus
|
||||
1280, // 720p
|
||||
1668, // Various iPads
|
||||
1920, // 1080p
|
||||
2048, // QXGA
|
||||
2560, // WQXGA
|
||||
3200, // QHD+
|
||||
3840, // 4K
|
||||
4480, // 4.5K
|
||||
5120, // 5K
|
||||
6016, // 6K
|
||||
],
|
||||
|
||||
formats: ['image/webp'],
|
||||
};
|
||||
|
||||
const computeHeight = (width: number, aspectRatio: number) => {
|
||||
return Math.floor(width / aspectRatio);
|
||||
};
|
||||
|
||||
const parseAspectRatio = (aspectRatio: number | string | null | undefined): number | undefined => {
|
||||
if (typeof aspectRatio === 'number') return aspectRatio;
|
||||
|
||||
if (typeof aspectRatio === 'string') {
|
||||
const match = aspectRatio.match(/(\d+)\s*[/:]\s*(\d+)/);
|
||||
|
||||
if (match) {
|
||||
const [, num, den] = match.map(Number);
|
||||
if (den && !isNaN(num)) return num / den;
|
||||
} else {
|
||||
const numericValue = parseFloat(aspectRatio);
|
||||
if (!isNaN(numericValue)) return numericValue;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the `sizes` attribute for an image, based on the layout and width
|
||||
*/
|
||||
export const getSizes = (width?: number, layout?: Layout): string | undefined => {
|
||||
if (!width || !layout) {
|
||||
return undefined;
|
||||
}
|
||||
switch (layout) {
|
||||
// If screen is wider than the max size, image width is the max size,
|
||||
// otherwise it's the width of the screen
|
||||
case `constrained`:
|
||||
return `(min-width: ${width}px) ${width}px, 100vw`;
|
||||
|
||||
// Image is always the same width, whatever the size of the screen
|
||||
case `fixed`:
|
||||
return `${width}px`;
|
||||
|
||||
// Image is always the width of the screen
|
||||
case `fullWidth`:
|
||||
return `100vw`;
|
||||
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const pixelate = (value?: number) => (value || value === 0 ? `${value}px` : undefined);
|
||||
|
||||
const getStyle = ({
|
||||
width,
|
||||
height,
|
||||
aspectRatio,
|
||||
layout,
|
||||
objectFit = 'cover',
|
||||
objectPosition = 'center',
|
||||
background,
|
||||
}: {
|
||||
width?: number;
|
||||
height?: number;
|
||||
aspectRatio?: number;
|
||||
objectFit?: string;
|
||||
objectPosition?: string;
|
||||
layout?: string;
|
||||
background?: string;
|
||||
}) => {
|
||||
const styleEntries: Array<[prop: string, value: string | undefined]> = [
|
||||
['object-fit', objectFit],
|
||||
['object-position', objectPosition],
|
||||
];
|
||||
|
||||
// If background is a URL, set it to cover the image and not repeat
|
||||
if (background?.startsWith('https:') || background?.startsWith('http:') || background?.startsWith('data:')) {
|
||||
styleEntries.push(['background-image', `url(${background})`]);
|
||||
styleEntries.push(['background-size', 'cover']);
|
||||
styleEntries.push(['background-repeat', 'no-repeat']);
|
||||
} else {
|
||||
styleEntries.push(['background', background]);
|
||||
}
|
||||
if (layout === 'fixed') {
|
||||
styleEntries.push(['width', pixelate(width)]);
|
||||
styleEntries.push(['height', pixelate(height)]);
|
||||
styleEntries.push(['object-position', 'top left']);
|
||||
}
|
||||
if (layout === 'constrained') {
|
||||
styleEntries.push(['max-width', pixelate(width)]);
|
||||
styleEntries.push(['max-height', pixelate(height)]);
|
||||
styleEntries.push(['aspect-ratio', aspectRatio ? `${aspectRatio}` : undefined]);
|
||||
styleEntries.push(['width', '100%']);
|
||||
}
|
||||
if (layout === 'fullWidth') {
|
||||
styleEntries.push(['width', '100%']);
|
||||
styleEntries.push(['aspect-ratio', aspectRatio ? `${aspectRatio}` : undefined]);
|
||||
styleEntries.push(['height', pixelate(height)]);
|
||||
}
|
||||
if (layout === 'responsive') {
|
||||
styleEntries.push(['width', '100%']);
|
||||
styleEntries.push(['height', 'auto']);
|
||||
styleEntries.push(['aspect-ratio', aspectRatio ? `${aspectRatio}` : undefined]);
|
||||
}
|
||||
if (layout === 'contained') {
|
||||
styleEntries.push(['max-width', '100%']);
|
||||
styleEntries.push(['max-height', '100%']);
|
||||
styleEntries.push(['object-fit', 'contain']);
|
||||
styleEntries.push(['aspect-ratio', aspectRatio ? `${aspectRatio}` : undefined]);
|
||||
}
|
||||
if (layout === 'cover') {
|
||||
styleEntries.push(['max-width', '100%']);
|
||||
styleEntries.push(['max-height', '100%']);
|
||||
}
|
||||
|
||||
const styles = Object.fromEntries(styleEntries.filter(([, value]) => value));
|
||||
|
||||
return Object.entries(styles)
|
||||
.map(([key, value]) => `${key}: ${value};`)
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
const getBreakpoints = ({
|
||||
width,
|
||||
breakpoints,
|
||||
layout,
|
||||
}: {
|
||||
width?: number;
|
||||
breakpoints?: number[];
|
||||
layout: Layout;
|
||||
}): number[] => {
|
||||
if (layout === 'fullWidth' || layout === 'cover' || layout === 'responsive' || layout === 'contained') {
|
||||
return breakpoints || config.deviceSizes;
|
||||
}
|
||||
if (!width) {
|
||||
return [];
|
||||
}
|
||||
const doubleWidth = width * 2;
|
||||
if (layout === 'fixed') {
|
||||
return [width, doubleWidth];
|
||||
}
|
||||
if (layout === 'constrained') {
|
||||
return [
|
||||
// Always include the image at 1x and 2x the specified width
|
||||
width,
|
||||
doubleWidth,
|
||||
// Filter out any resolutions that are larger than the double-res image
|
||||
...(breakpoints || config.deviceSizes).filter((w) => w < doubleWidth),
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
/* ** */
|
||||
export const astroAsseetsOptimizer: ImagesOptimizer = async (image, breakpoints) => {
|
||||
if (!image || typeof image === 'string') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
breakpoints.map(async (w: number) => {
|
||||
const url = (await getImage({ src: image, width: w })).src;
|
||||
return {
|
||||
src: url,
|
||||
width: w,
|
||||
};
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
/* ** */
|
||||
export const unpicOptimizer: ImagesOptimizer = async (image, breakpoints, width, height) => {
|
||||
if (!image || typeof image !== 'string') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const urlParsed = parseUrl(image);
|
||||
if (!urlParsed) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
breakpoints.map(async (w: number) => {
|
||||
const url =
|
||||
(await transformUrl({
|
||||
url: image,
|
||||
width: w,
|
||||
height: width && height ? computeHeight(w, width / height) : height,
|
||||
cdn: urlParsed.cdn,
|
||||
})) || image;
|
||||
return {
|
||||
src: String(url),
|
||||
width: w,
|
||||
};
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
/* ** */
|
||||
export async function getImagesOptimized(
|
||||
image: ImageMetadata | string,
|
||||
{ src: _, width, height, sizes, aspectRatio, widths, layout = 'constrained', style = '', ...rest }: ImageProps,
|
||||
transform: ImagesOptimizer = () => Promise.resolve([])
|
||||
): Promise<{ src: string; attributes: AttributesProps }> {
|
||||
if (typeof image !== 'string') {
|
||||
width ||= Number(image.width) || undefined;
|
||||
height ||= typeof width === 'number' ? computeHeight(width, image.width / image.height) : undefined;
|
||||
}
|
||||
|
||||
width = (width && Number(width)) || undefined;
|
||||
height = (height && Number(height)) || undefined;
|
||||
|
||||
widths ||= config.deviceSizes;
|
||||
sizes ||= getSizes(Number(width) || undefined, layout);
|
||||
aspectRatio = parseAspectRatio(aspectRatio);
|
||||
|
||||
// Calculate dimensions from aspect ratio
|
||||
if (aspectRatio) {
|
||||
if (width) {
|
||||
if (height) {
|
||||
/* empty */
|
||||
} else {
|
||||
height = width / aspectRatio;
|
||||
}
|
||||
} else if (height) {
|
||||
width = Number(height * aspectRatio);
|
||||
} else if (layout !== 'fullWidth') {
|
||||
// Fullwidth images have 100% width, so aspectRatio is applicable
|
||||
console.error('When aspectRatio is set, either width or height must also be set');
|
||||
console.error('Image', image);
|
||||
}
|
||||
} else if (width && height) {
|
||||
aspectRatio = width / height;
|
||||
} else if (layout !== 'fullWidth') {
|
||||
// Fullwidth images don't need dimensions
|
||||
console.error('Either aspectRatio or both width and height must be set');
|
||||
console.error('Image', image);
|
||||
}
|
||||
|
||||
let breakpoints = getBreakpoints({ width: width, breakpoints: widths, layout: layout });
|
||||
breakpoints = [...new Set(breakpoints)].sort((a, b) => a - b);
|
||||
|
||||
const srcset = (await transform(image, breakpoints, Number(width) || undefined, Number(height) || undefined))
|
||||
.map(({ src, width }) => `${src} ${width}w`)
|
||||
.join(', ');
|
||||
|
||||
return {
|
||||
src: typeof image === 'string' ? image : image.src,
|
||||
attributes: {
|
||||
width: width,
|
||||
height: height,
|
||||
srcset: srcset || undefined,
|
||||
sizes: sizes,
|
||||
style: `${getStyle({
|
||||
width: width,
|
||||
height: height,
|
||||
aspectRatio: aspectRatio,
|
||||
layout: layout,
|
||||
})}${style ?? ''}`,
|
||||
...rest,
|
||||
},
|
||||
};
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
import { getImage } from '@astrojs/image';
|
||||
import type { OpenGraph } from '@astrolib/seo/src/types';
|
||||
import { getImage } from 'astro:assets';
|
||||
import type { ImageMetadata } from 'astro';
|
||||
import type { OpenGraph } from '@astrolib/seo/src/types';
|
||||
|
||||
const load = async function () {
|
||||
let images: Record<string, () => Promise<unknown>> | undefined = undefined;
|
||||
try {
|
||||
images = import.meta.glob('~/assets/images/**');
|
||||
images = import.meta.glob('~/assets/images/**/*.{jpeg,jpg,png,tiff,webp,gif,svg,JPEG,JPG,PNG,TIFF,WEBP,GIF,SVG}');
|
||||
} catch (e) {
|
||||
// continue regardless of error
|
||||
}
|
||||
@ -21,24 +21,27 @@ export const fetchLocalImages = async () => {
|
||||
};
|
||||
|
||||
/** */
|
||||
export const findImage = async (imagePath?: string) => {
|
||||
export const findImage = async (imagePath?: string | ImageMetadata | null): Promise<string | ImageMetadata | undefined | null> => {
|
||||
// Not string
|
||||
if (typeof imagePath !== 'string') {
|
||||
return null;
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
// Absolute paths
|
||||
if (imagePath.startsWith('http://') || imagePath.startsWith('https://') || imagePath.startsWith('/')) {
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
if (!imagePath.startsWith('~/assets')) {
|
||||
return null;
|
||||
} // For now only consume images using ~/assets alias (or absolute)
|
||||
// Relative paths or not "~/assets/"
|
||||
if (!imagePath.startsWith('~/assets/images')) {
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
const images = await fetchLocalImages();
|
||||
const key = imagePath.replace('~/', '/src/');
|
||||
|
||||
return images && typeof images[key] === 'function'
|
||||
? ((await images[key]()) as { default: unknown })['default']
|
||||
? ((await images[key]()) as { default: ImageMetadata })['default']
|
||||
: null;
|
||||
};
|
||||
|
||||
|
53
src/utils/tasks.mjs
Normal file
@ -0,0 +1,53 @@
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
|
||||
const tasksIntegration = () => {
|
||||
let config;
|
||||
return {
|
||||
name: 'AstroWind:tasks',
|
||||
|
||||
hooks: {
|
||||
'astro:config:done': async ({ config: cfg }) => {
|
||||
config = cfg;
|
||||
},
|
||||
|
||||
'astro:build:done': async () => {
|
||||
try {
|
||||
const outDir = config.outDir;
|
||||
const publicDir = config.publicDir;
|
||||
const sitemapName = 'sitemap-index.xml';
|
||||
const sitemapFile = new URL(sitemapName, outDir);
|
||||
const robotsTxtFile = new URL('robots.txt', publicDir);
|
||||
const robotsTxtFileInOut = new URL('robots.txt', outDir);
|
||||
|
||||
const hasIntegration =
|
||||
Array.isArray(config?.integrations) &&
|
||||
config.integrations?.find((e) => e?.name === '@astrojs/sitemap') !== undefined;
|
||||
const sitemapExists = fs.existsSync(sitemapFile);
|
||||
|
||||
if (hasIntegration && sitemapExists) {
|
||||
const robotsTxt = fs.readFileSync(robotsTxtFile, { encoding: 'utf8', flags: 'a+' });
|
||||
const sitemapUrl = new URL(sitemapName, String(new URL(config.base, config.site)));
|
||||
const pattern = /^Sitemap:(.*)$/m;
|
||||
|
||||
if (!pattern.test(robotsTxt)) {
|
||||
fs.appendFileSync(robotsTxtFileInOut, `${os.EOL}${os.EOL}Sitemap: ${sitemapUrl}`, {
|
||||
encoding: 'utf8',
|
||||
flags: 'w',
|
||||
});
|
||||
} else {
|
||||
fs.writeFileSync(robotsTxtFileInOut, robotsTxt.replace(pattern, `Sitemap: ${sitemapUrl}`), {
|
||||
encoding: 'utf8',
|
||||
flags: 'w',
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
/* empty */
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default tasksIntegration;
|