diff --git a/README.md b/README.md index ae4e47b..153043a 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/astro.config.mjs b/astro.config.mjs index 7e3db13..60b35bf 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -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: { diff --git a/package.json b/package.json index 612d1b5..6722697 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/src/assets/favicons/apple-touch-icon.png b/src/assets/favicons/apple-touch-icon.png new file mode 100644 index 0000000..6262d06 Binary files /dev/null and b/src/assets/favicons/apple-touch-icon.png differ diff --git a/src/assets/favicons/favicon.ico b/src/assets/favicons/favicon.ico index 578ad45..9f9502a 100644 Binary files a/src/assets/favicons/favicon.ico and b/src/assets/favicons/favicon.ico differ diff --git a/src/assets/images/caos.jpg b/src/assets/images/caos.jpg deleted file mode 100644 index 491099b..0000000 Binary files a/src/assets/images/caos.jpg and /dev/null differ diff --git a/src/assets/images/colors.jpg b/src/assets/images/colors.jpg deleted file mode 100644 index e104e62..0000000 Binary files a/src/assets/images/colors.jpg and /dev/null differ diff --git a/src/assets/images/creativity.jpg b/src/assets/images/creativity.jpg deleted file mode 100644 index 6a07991..0000000 Binary files a/src/assets/images/creativity.jpg and /dev/null differ diff --git a/src/assets/images/do-more.jpg b/src/assets/images/do-more.jpg deleted file mode 100644 index 6ede197..0000000 Binary files a/src/assets/images/do-more.jpg and /dev/null differ diff --git a/src/assets/images/stickers.jpg b/src/assets/images/stickers.jpg deleted file mode 100644 index 0c7dbd1..0000000 Binary files a/src/assets/images/stickers.jpg and /dev/null differ diff --git a/src/assets/images/tools.jpg b/src/assets/images/tools.jpg deleted file mode 100644 index 4af0ee9..0000000 Binary files a/src/assets/images/tools.jpg and /dev/null differ diff --git a/src/assets/images/vintage.jpg b/src/assets/images/vintage.jpg deleted file mode 100644 index aa4e5d0..0000000 Binary files a/src/assets/images/vintage.jpg and /dev/null differ diff --git a/src/assets/styles/tailwind.css b/src/assets/styles/tailwind.css index b16ad97..bacd14e 100644 --- a/src/assets/styles/tailwind.css +++ b/src/assets/styles/tailwind.css @@ -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 { diff --git a/src/components/Favicons.astro b/src/components/Favicons.astro index 748693a..fed6696 100644 --- a/src/components/Favicons.astro +++ b/src/components/Favicons.astro @@ -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'; --- - \ No newline at end of file + + diff --git a/src/components/blog/GridItem.astro b/src/components/blog/GridItem.astro index f3d0dcb..3f4a5f1 100644 --- a/src/components/blog/GridItem.astro +++ b/src/components/blog/GridItem.astro @@ -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)); ---
@@ -21,15 +21,15 @@ const image = (await findImage(post.image)) as ImageMetadata | undefined; { image && ( - diff --git a/src/components/blog/ListItem.astro b/src/components/blog/ListItem.astro index 97ef26d..61111c9 100644 --- a/src/components/blog/ListItem.astro +++ b/src/components/blog/ListItem.astro @@ -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 )) diff --git a/src/components/widgets/Steps.astro b/src/components/widgets/Steps.astro index deb9a1e..652efbe 100644 --- a/src/components/widgets/Steps.astro +++ b/src/components/widgets/Steps.astro @@ -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' ? ( ) : ( - diff --git a/src/components/widgets/Testimonials.astro b/src/components/widgets/Testimonials.astro index 7c4558c..c5db56a 100644 --- a/src/components/widgets/Testimonials.astro +++ b/src/components/widgets/Testimonials.astro @@ -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' ? ( ) : ( - )} diff --git a/src/content/post/astrowind-template-in-depth.mdx b/src/content/post/astrowind-template-in-depth.mdx index 9b8b1e9..5483700 100644 --- a/src/content/post/astrowind-template-in-depth.mdx +++ b/src/content/post/astrowind-template-in-depth.mdx @@ -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 diff --git a/src/content/post/get-started-website-with-astro-tailwind-css.md b/src/content/post/get-started-website-with-astro-tailwind-css.md index ad0843e..42791da 100644 --- a/src/content/post/get-started-website-with-astro-tailwind-css.md +++ b/src/content/post/get-started-website-with-astro-tailwind-css.md @@ -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 diff --git a/src/content/post/how-to-customize-astrowind-to-your-brand.md b/src/content/post/how-to-customize-astrowind-to-your-brand.md index fb50795..ddf1607 100644 --- a/src/content/post/how-to-customize-astrowind-to-your-brand.md +++ b/src/content/post/how-to-customize-astrowind-to-your-brand.md @@ -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 diff --git a/src/content/post/useful-resources-to-create-websites.md b/src/content/post/useful-resources-to-create-websites.md index 9a943fa..4589c0b 100644 --- a/src/content/post/useful-resources-to-create-websites.md +++ b/src/content/post/useful-resources-to-create-websites.md @@ -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 diff --git a/src/env.d.ts b/src/env.d.ts index 69ebcf8..9afcd11 100644 --- a/src/env.d.ts +++ b/src/env.d.ts @@ -1,3 +1,3 @@ // eslint-disable-next-line @typescript-eslint/triple-slash-reference /// -/// +/// diff --git a/src/pages/[...blog]/[...page].astro b/src/pages/[...blog]/[...page].astro index 1810178..8260889 100644 --- a/src/pages/[...blog]/[...page].astro +++ b/src/pages/[...blog]/[...page].astro @@ -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; diff --git a/src/pages/[...blog]/[category]/[...page].astro b/src/pages/[...blog]/[category]/[...page].astro index a15be82..1ff5cd6 100644 --- a/src/pages/[...blog]/[category]/[...page].astro +++ b/src/pages/[...blog]/[category]/[...page].astro @@ -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; diff --git a/src/pages/[...blog]/[tag]/[...page].astro b/src/pages/[...blog]/[tag]/[...page].astro index 8c4adc1..20f21db 100644 --- a/src/pages/[...blog]/[tag]/[...page].astro +++ b/src/pages/[...blog]/[tag]/[...page].astro @@ -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; diff --git a/src/pages/[...blog]/index.astro b/src/pages/[...blog]/index.astro index a8a86da..6612897 100644 --- a/src/pages/[...blog]/index.astro +++ b/src/pages/[...blog]/index.astro @@ -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 } : {}) } diff --git a/src/pages/about.astro b/src/pages/about.astro index 67cc440..f4181ab 100644 --- a/src/pages/about.astro +++ b/src/pages/about.astro @@ -14,7 +14,7 @@ const metadata = { - + Elevate your online presence with our
Beautiful Website Templates @@ -112,7 +112,7 @@ const metadata = { }, ]} image={{ - src: import('~/assets/images/colors.jpg'), + src: '~/assets/images/colors.jpg', alt: 'Colorful Image', }} /> diff --git a/src/pages/index.astro b/src/pages/index.astro index 596e468..9e07068 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -24,7 +24,7 @@ const metadata = { Free template for @@ -96,8 +96,9 @@ const metadata = { @@ -133,6 +134,43 @@ const metadata = { + + +

Ad vix debet docendi

+ Ne dicta praesent ocurreret has, diam theophrastus at pro. Eos etiam regione ut, persius eripuit quo id. Sit te + euismod tacimates. +
+ + +
+
+
+ + + @@ -174,19 +212,19 @@ const metadata = { { title: 'Step 1: Download', 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: Add content', 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: Customize styles', 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: diff --git a/src/pages/landing/mobile-app.astro b/src/pages/landing/mobile-app.astro index 5bd925c..d0f4257 100644 --- a/src/pages/landing/mobile-app.astro +++ b/src/pages/landing/mobile-app.astro @@ -40,7 +40,7 @@ const metadata = { Free template for @@ -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 = { @@ -140,7 +141,6 @@ const metadata = { @@ -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', }, }, diff --git a/src/pages/landing/saas.astro b/src/pages/landing/saas.astro index 52c3329..50e7314 100644 --- a/src/pages/landing/saas.astro +++ b/src/pages/landing/saas.astro @@ -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 = { Free template for
@@ -56,6 +57,51 @@ const metadata = {
+ + + + @@ -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', }} > diff --git a/src/pages/landing/startup.astro b/src/pages/landing/startup.astro index 0db6b20..0f06c3b 100644 --- a/src/pages/landing/startup.astro +++ b/src/pages/landing/startup.astro @@ -43,14 +43,18 @@ const metadata = {
- +
diff --git a/src/pages/pricing.astro b/src/pages/pricing.astro index 6e62aed..2a6f817 100644 --- a/src/pages/pricing.astro +++ b/src/pages/pricing.astro @@ -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', }} /> diff --git a/src/pages/rss.xml.ts b/src/pages/rss.xml.ts index ba1117d..0fa45d6 100644 --- a/src/pages/rss.xml.ts +++ b/src/pages/rss.xml.ts @@ -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) => ({ diff --git a/src/pages/services.astro b/src/pages/services.astro index c361266..d44661c 100644 --- a/src/pages/services.astro +++ b/src/pages/services.astro @@ -27,7 +27,7 @@ const metadata = {
@@ -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', }, }, diff --git a/src/utils/blog.ts b/src/utils/blog.ts index 5be45da..24ab7dd 100644 --- a/src/utils/blog.ts +++ b/src/utils/blog.ts @@ -162,18 +162,16 @@ export const findLatestPosts = async ({ count }: { count?: number }): Promise - async ({ paginate }) => { - if (!isBlogEnabled || !isBlogListRouteEnabled) return []; - return paginate(await fetchPosts(), { - params: { blog: BLOG_BASE || undefined }, - pageSize: blogPostsPerPage, - }); - }; +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,49 +182,45 @@ export const getStaticPathsBlogPost = () => async () => { }; /** */ -export const getStaticPathsBlogCategory = - () => - async ({ paginate }) => { - if (!isBlogEnabled || !isBlogCategoryRouteEnabled) return []; +export const getStaticPathsBlogCategory = async ({ paginate }) => { + if (!isBlogEnabled || !isBlogCategoryRouteEnabled) return []; - const posts = await fetchPosts(); - const categories = new Set(); - posts.map((post) => { - typeof post.category === 'string' && categories.add(post.category.toLowerCase()); - }); + const posts = await fetchPosts(); + const categories = new Set(); + posts.map((post) => { + typeof post.category === 'string' && categories.add(post.category.toLowerCase()); + }); - return Array.from(categories).map((category: string) => - paginate( - posts.filter((post) => typeof post.category === 'string' && category === post.category.toLowerCase()), - { - params: { category: category, blog: CATEGORY_BASE || undefined }, - pageSize: blogPostsPerPage, - props: { category }, - } - ) - ); - }; + return Array.from(categories).map((category: string) => + paginate( + posts.filter((post) => typeof post.category === 'string' && category === post.category.toLowerCase()), + { + params: { category: category, blog: CATEGORY_BASE || undefined }, + pageSize: blogPostsPerPage, + props: { category }, + } + ) + ); +}; /** */ -export const getStaticPathsBlogTag = - () => - async ({ paginate }) => { - if (!isBlogEnabled || !isBlogTagRouteEnabled) return []; +export const getStaticPathsBlogTag = async ({ paginate }) => { + if (!isBlogEnabled || !isBlogTagRouteEnabled) return []; - const posts = await fetchPosts(); - const tags = new Set(); - posts.map((post) => { - Array.isArray(post.tags) && post.tags.map((tag) => tags.add(tag.toLowerCase())); - }); + const posts = await fetchPosts(); + const tags = new Set(); + posts.map((post) => { + Array.isArray(post.tags) && post.tags.map((tag) => tags.add(tag.toLowerCase())); + }); - return Array.from(tags).map((tag: string) => - paginate( - posts.filter((post) => Array.isArray(post.tags) && post.tags.find((elem) => elem.toLowerCase() === tag)), - { - params: { tag: tag, blog: TAG_BASE || undefined }, - pageSize: blogPostsPerPage, - props: { tag }, - } - ) - ); - }; + return Array.from(tags).map((tag: string) => + paginate( + posts.filter((post) => Array.isArray(post.tags) && post.tags.find((elem) => elem.toLowerCase() === tag)), + { + params: { tag: tag, blog: TAG_BASE || undefined }, + pageSize: blogPostsPerPage, + props: { tag }, + } + ) + ); +}; diff --git a/src/utils/images-optimization.ts b/src/utils/images-optimization.ts new file mode 100644 index 0000000..a465c94 --- /dev/null +++ b/src/utils/images-optimization.ts @@ -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, '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>; + +/* ******* */ +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, + }, + }; +} diff --git a/src/utils/images.ts b/src/utils/images.ts index bd8bacb..daafedb 100644 --- a/src/utils/images.ts +++ b/src/utils/images.ts @@ -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 Promise> | 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 => { + // 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; }; @@ -91,4 +94,4 @@ export const adaptOpenGraphImages = async ( ); return { ...openGraph, ...(adaptedImages ? { images: adaptedImages } : {}) }; -}; +}; \ No newline at end of file diff --git a/src/utils/tasks.mjs b/src/utils/tasks.mjs new file mode 100644 index 0000000..9e17809 --- /dev/null +++ b/src/utils/tasks.mjs @@ -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;