Migrate from @astrojs/image to Astro Assets and Unpic
@ -5,7 +5,6 @@ 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';
|
||||
@ -37,9 +36,7 @@ export default defineConfig({
|
||||
applyBaseStyles: false,
|
||||
}),
|
||||
sitemap(),
|
||||
image({
|
||||
serviceEntryPoint: '@astrojs/image/sharp',
|
||||
}),
|
||||
|
||||
mdx(),
|
||||
icon({
|
||||
include: {
|
||||
@ -72,6 +69,10 @@ export default defineConfig({
|
||||
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",
|
||||
@ -45,7 +44,8 @@
|
||||
"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"
|
||||
|
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 |
@ -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';
|
||||
|
||||
const {
|
||||
title = await Astro.slots.render('title'),
|
||||
@ -77,11 +77,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}
|
||||
|
@ -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-1516216628859-9bccecab13ca?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2069&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" />
|
||||
|
@ -28,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>
|
||||
@ -116,7 +116,7 @@ const metadata = {
|
||||
},
|
||||
]}
|
||||
image={{
|
||||
src: import('~/assets/images/caos.jpg'),
|
||||
src: 'https://images.unsplash.com/photo-1517134191118-9d595e4c8c2b?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80',
|
||||
alt: 'Colorful Image',
|
||||
}}
|
||||
>
|
||||
@ -157,7 +157,7 @@ const metadata = {
|
||||
},
|
||||
]}
|
||||
image={{
|
||||
src: import('~/assets/images/vintage.jpg'),
|
||||
src: 'https://images.unsplash.com/photo-1483058712412-4245e9b90334?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=2070&q=80',
|
||||
alt: 'Vintage Image',
|
||||
}}
|
||||
>
|
||||
@ -195,7 +195,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',
|
||||
}}
|
||||
/>
|
||||
|
@ -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">
|
||||
<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"
|
||||
allowfullscreen
|
||||
style="width:100%"></iframe>
|
||||
<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;"
|
||||
allowfullscreen
|
||||
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',
|
||||
}}
|
||||
/>
|
||||
|
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;
|
||||
};
|
||||
|
||||
|