Add support for new config.yaml

This commit is contained in:
prototypa
2023-07-27 21:52:04 -04:00
parent 8c4698412e
commit d6f3055e31
54 changed files with 860 additions and 591 deletions

112
README.md
View File

@ -108,7 +108,7 @@ Inside AstroWind template, you'll see the following folders and files:
│ │ ├-- rss.xml.ts │ │ ├-- rss.xml.ts
│ │ └── ... │ │ └── ...
│ ├── utils/ │ ├── utils/
│ ├── config.mjs │ ├── config.yaml
│ └── navigation.js │ └── navigation.js
├── package.json ├── package.json
├── astro.config.mjs ├── astro.config.mjs
@ -145,65 +145,79 @@ All commands are run from the root of the project, from a terminal:
### Configuration ### Configuration
Basic configuration file: `./src/config.mjs` Basic configuration file: `./src/config.yaml`
```javascript ```yaml
const CONFIG = { site:
name: 'Example', name: AstroWind
site: 'https://astrowind.vercel.app'
base: '/' # Change this if you need to deploy to Github Pages, for example
trailingSlash: false # Generate permalinks with or without "/" at the end
origin: 'https://example.com', googleSiteVerificationId: orcPxI47GSa-cRvY11tUe6iGg2IO_RPvnA1q95iEM3M
basePathname: '/', // Change this if you need to deploy to Github Pages, for example
trailingSlash: false, // Generate permalinks with or without "/" at the end
title: 'Example - This is the homepage title of Example', // Default seo title # Default SEO metadata
description: 'This is the homepage description of Example', // Default seo description metadata:
defaultImage: 'image.jpg', // Default seo image title:
default: AstroWind
template: '%s — AstroWind'
description: "\U0001F680 Suitable for Startups, Small Business, Sass Websites, Professional Portfolios, Marketing Websites, Landing Pages & Blogs."
robots:
index: true
follow: true
openGraph:
siteName: AstroWind
images:
- url: '~/assets/images/default.jpg'
width: 1200
height: 628
type: website
twitter:
handle: '@onwidget'
site: '@onwidget'
cardType: summary_large_image
defaultTheme: 'system', // Values: "system" | "light" | "dark" | "light:only" | "dark:only" i18n:
language: en
textDirection: ltr
language: 'en', // Default language apps:
textDirection: 'ltr', // Default html text direction blog:
isEnabled: true
postsPerPage: 6
dateFormatter: new Intl.DateTimeFormat('en', { post:
// Date format isEnabled: true
year: 'numeric', permalink: '/%slug%' # Variables: %slug%, %year%, %month%, %day%, %hour%, %minute%, %second%, %category%
month: 'short', robots:
day: 'numeric', index: true
timeZone: 'UTC',
}),
googleAnalyticsId: false, // Or "G-XXXXXXXXXX", list:
googleSiteVerificationId: false, // Or some value, isEnabled: true
pathname: 'blog' # Blog main path, you can change this to "articles" (/articles)
robots:
index: true
blog: { category:
disabled: false, isEnabled: true
postsPerPage: 4, pathname: 'category' # Category main path /category/some-category, you can change this to "group" (/group/some-category)
robots:
index: true
post: { tag:
permalink: '/%slug%', // variables: %slug%, %year%, %month%, %day%, %hour%, %minute%, %second%, %category% isEnabled: true
noindex: false, pathname: 'tag' # Tag main path /tag/some-tag, you can change this to "topics" (/topics/some-category)
disabled: false, robots:
}, index: false
list: { analytics:
pathname: 'blog', // Blog main path, you can change this to "articles" (/articles) vendors:
noindex: false, googleAnalytics:
disabled: false, isEnabled: false
}, id: null # or "G-XXXXXXXXXX"
category: { ui:
pathname: 'category', // Category main path /category/some-category theme: 'system' # Values: "system" | "light" | "dark" | "light:only" | "dark:only"
noindex: true,
disabled: false,
},
tag: {
pathname: 'tag', // Tag main path /tag/some-tag
noindex: true,
disabled: false,
},
},
};
``` ```
<br> <br>

View File

@ -6,6 +6,15 @@
margin-top: 0; margin-top: 0;
} }
@layer utilities {
.bg-page {
background-color: var(--aw-color-bg-page);
}
.bg-dark {
background-color: var(--aw-color-bg-page-dark);
}
}
@layer components { @layer components {
.text-page { .text-page {
color: var(--aw-color-text-page); color: var(--aw-color-text-page);
@ -19,10 +28,6 @@
background-color: var(--aw-color-bg-page); background-color: var(--aw-color-bg-page);
} }
.bg-dark {
@apply bg-slate-900;
}
.btn { .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 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;
} }

View File

@ -28,8 +28,27 @@ import '@fontsource-variable/inter';
--aw-color-primary: rgb(30 64 175); --aw-color-primary: rgb(30 64 175);
--aw-color-secondary: rgb(30 58 138); --aw-color-secondary: rgb(30 58 138);
--aw-color-accent: rgb(109 40 217); --aw-color-accent: rgb(109 40 217);
--aw-color-text-page: rgb(17 24 39);
--aw-color-text-heading: rgb(0 0 0);
--aw-color-text-default: rgb(16 16 16);
--aw-color-text-muted: rgb(16 16 16 / 66%); --aw-color-text-muted: rgb(16 16 16 / 66%);
--aw-color-bg-page: rgb(255 255 255); --aw-color-bg-page: rgb(255 255 255);
--aw-color-bg-page-dark: rgb(3 6 32);
}
.dark {
--aw-font-sans: 'Inter Variable';
--aw-font-serif: var(--aw-font-sans);
--aw-font-heading: var(--aw-font-sans);
--aw-color-primary: rgb(30 64 175);
--aw-color-secondary: rgb(30 58 138);
--aw-color-accent: rgb(109 40 217);
--aw-color-text-heading: rgb(0 0 0);
--aw-color-text-default: rgb(229 236 246);
--aw-color-text-muted: rgb(229 236 246 / 66%);
--aw-color-bg-page: var(--aw-color-bg-page-dark);
} }
</style> </style>

View File

@ -1,7 +1,7 @@
--- ---
import { SITE } from '~/config.mjs'; import { SITE_CONFIG } from '~/utils/config';
--- ---
<span class="self-center ml-2 text-2xl md:text-xl font-bold text-gray-900 whitespace-nowrap dark:text-white"> <span class="self-center ml-2 text-2xl md:text-xl font-bold text-gray-900 whitespace-nowrap dark:text-white">
🚀 {SITE?.name} 🚀 {SITE_CONFIG?.name}
</span> </span>

View File

@ -1,7 +1,8 @@
--- ---
import { Picture } from '@astrojs/image/components'; import { Picture } from '@astrojs/image/components';
import type { ImageMetadata } from 'astro';
import { BLOG } from '~/config.mjs'; import { APP_BLOG_CONFIG } from '~/utils/config';
import type { Post } from '~/types'; import type { Post } from '~/types';
import { findImage } from '~/utils/images'; import { findImage } from '~/utils/images';
@ -12,7 +13,7 @@ export interface Props {
} }
const { post } = Astro.props; const { post } = Astro.props;
const image = await findImage(post.image); const image = (await findImage(post.image)) as ImageMetadata | undefined;
--- ---
<article class="mb-6 transition"> <article class="mb-6 transition">
@ -38,7 +39,7 @@ const image = await findImage(post.image);
</div> </div>
<h3 class="mb-2 text-xl font-bold leading-tight sm:text-2xl font-heading"> <h3 class="mb-2 text-xl font-bold leading-tight sm:text-2xl font-heading">
{ {
BLOG?.post?.disabled ? ( !APP_BLOG_CONFIG?.post?.isEnabled ? (
post.title post.title
) : ( ) : (
<a <a
@ -50,5 +51,5 @@ const image = await findImage(post.image);
) )
} }
</h3> </h3>
<p class="text-muted dark:text-slate-400 text-lg">{post.excerpt || post.description}</p> <p class="text-muted dark:text-slate-400 text-lg">{post.excerpt}</p>
</article> </article>

View File

@ -1,53 +0,0 @@
---
import Grid from '~/components/blog/Grid.astro';
import { getBlogPermalink } from '~/utils/permalinks';
import { findPostsByIds } from '~/utils/blog';
export interface Props {
title?: string;
allPostsText?: string;
allPostsLink?: string | URL;
information?: string;
postIds: string[];
}
const {
title = await Astro.slots.render('title'),
allPostsText = 'View all posts',
allPostsLink = getBlogPermalink(),
information = await Astro.slots.render('information'),
postIds = [],
} = Astro.props;
const posts = await findPostsByIds(postIds);
---
<section class="px-4 py-16 mx-auto max-w-7xl lg:py-20">
<div class="flex flex-col lg:justify-between lg:flex-row mb-8">
<div class="md:max-w-sm">
{
title && (
<h2
class="text-3xl font-bold tracking-tight sm:text-4xl sm:leading-none group font-heading mb-2"
set:html={title}
/>
)
}
{
allPostsText && allPostsLink && (
<a
class="text-muted dark:text-slate-400 hover:text-primary transition ease-in duration-200 block mb-6 md:mb-0"
href={allPostsLink}
>
{allPostsText} »
</a>
)
}
</div>
{information && <p class="text-muted dark:text-slate-400 lg:text-sm lg:max-w-md" set:html={information} />}
</div>
<Grid posts={posts} />
</section>

View File

@ -1,53 +0,0 @@
---
import Grid from '~/components/blog/Grid.astro';
import { getBlogPermalink } from '~/utils/permalinks';
import { findLatestPosts } from '~/utils/blog';
export interface Props {
title?: string;
allPostsText?: string;
allPostsLink?: string | URL;
information?: string;
count?: number;
}
const {
title = await Astro.slots.render('title'),
allPostsText = 'View all posts',
allPostsLink = getBlogPermalink(),
information = await Astro.slots.render('information'),
count = 4,
} = Astro.props;
const posts = await findLatestPosts({ count });
---
<section class="px-4 py-16 mx-auto max-w-7xl lg:py-20">
<div class="flex flex-col lg:justify-between lg:flex-row mb-8">
<div class="md:max-w-sm">
{
title && (
<h2
class="text-3xl font-bold tracking-tight sm:text-4xl sm:leading-none group font-heading mb-2"
set:html={title}
/>
)
}
{
allPostsText && allPostsLink && (
<a
class="text-muted dark:text-slate-400 hover:text-primary transition ease-in duration-200 block mb-6 lg:mb-0"
href={allPostsLink}
>
{allPostsText} »
</a>
)
}
</div>
{information && <p class="text-muted dark:text-slate-400 lg:text-sm lg:max-w-md" set:html={information} />}
</div>
<Grid posts={posts} />
</section>

View File

@ -1,9 +1,10 @@
--- ---
import { Icon } from 'astro-icon/components';
import { Picture } from '@astrojs/image/components'; import { Picture } from '@astrojs/image/components';
import type { ImageMetadata } from 'astro';
import { Icon } from 'astro-icon/components';
import PostTags from '~/components/blog/Tags.astro'; import PostTags from '~/components/blog/Tags.astro';
import { BLOG } from '~/config.mjs'; import { APP_BLOG_CONFIG } from '~/utils/config';
import type { Post } from '~/types'; import type { Post } from '~/types';
import { getPermalink } from '~/utils/permalinks'; import { getPermalink } from '~/utils/permalinks';
@ -15,9 +16,9 @@ export interface Props {
} }
const { post } = Astro.props; const { post } = Astro.props;
const image = await findImage(post.image); const image = (await findImage(post.image)) as ImageMetadata | undefined;
const link = !BLOG?.post?.disabled ? getPermalink(post.permalink, 'post') : ''; const link = APP_BLOG_CONFIG?.post?.isEnabled ? getPermalink(post.permalink, 'post') : '';
--- ---
<article class={`max-w-md mx-auto md:max-w-none grid gap-6 md:gap-8 ${image ? 'md:grid-cols-2' : ''}`}> <article class={`max-w-md mx-auto md:max-w-none grid gap-6 md:gap-8 ${image ? 'md:grid-cols-2' : ''}`}>

View File

@ -16,6 +16,7 @@ export interface Props {
} }
const { post, url } = Astro.props; const { post, url } = Astro.props;
const Content = post?.Content || null;
--- ---
<section class="py-8 sm:py-16 lg:py-20 mx-auto"> <section class="py-8 sm:py-16 lg:py-20 mx-auto">
@ -57,7 +58,7 @@ const { post, url } = Astro.props;
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-6xl mx-auto mb-6 sm:rounded-md bg-gray-400 dark:bg-slate-700"
widths={[400, 900]} widths={[400, 900]}
sizes="(max-width: 900px) 400px, 900px" sizes="(max-width: 900px) 400px, 900px"
alt={post.description || ''} alt={post?.excerpt || ''}
loading="eager" loading="eager"
aspectRatio={16 / 9} aspectRatio={16 / 9}
width={900} width={900}
@ -77,11 +78,8 @@ const { post, url } = Astro.props;
class="mx-auto px-6 sm:px-6 max-w-3xl prose prose-lg lg:prose-xl dark:prose-invert dark:prose-headings:text-slate-300 prose-md prose-headings:font-heading prose-headings:leading-tighter prose-headings:tracking-tighter prose-headings:font-bold prose-a:text-primary dark:prose-a:text-blue-400 prose-img:rounded-md prose-img:shadow-lg mt-8" class="mx-auto px-6 sm:px-6 max-w-3xl prose prose-lg lg:prose-xl dark:prose-invert dark:prose-headings:text-slate-300 prose-md prose-headings:font-heading prose-headings:leading-tighter prose-headings:tracking-tighter prose-headings:font-bold prose-a:text-primary dark:prose-a:text-blue-400 prose-img:rounded-md prose-img:shadow-lg mt-8"
> >
{ {
post.Content ? ( Content ? (
<> <Content />
{/* @ts-ignore */}
<post.Content />
</>
) : ( ) : (
<Fragment set:html={post.content} /> <Fragment set:html={post.content} />
) )

View File

@ -1,7 +1,7 @@
--- ---
import { getPermalink } from '~/utils/permalinks'; import { getPermalink } from '~/utils/permalinks';
import { BLOG } from '~/config.mjs'; import { APP_BLOG_CONFIG } from '~/utils/config';
import type { Post } from '~/types'; import type { Post } from '~/types';
export interface Props { export interface Props {
@ -23,7 +23,7 @@ const { tags, class: className = 'text-sm', title = undefined, isCategory = fals
<ul class={className}> <ul class={className}>
{tags.map((tag) => ( {tags.map((tag) => (
<li class="bg-gray-100 dark:bg-slate-700 inline-block mr-2 mb-2 py-0.5 px-2 lowercase font-medium"> <li class="bg-gray-100 dark:bg-slate-700 inline-block mr-2 mb-2 py-0.5 px-2 lowercase font-medium">
{BLOG?.tag?.disabled ? ( {!APP_BLOG_CONFIG?.tag?.isEnabled ? (
tag tag
) : ( ) : (
<a <a

View File

@ -0,0 +1,8 @@
---
import { GoogleAnalytics } from '@astrolib/analytics';
import { ANALYTICS_CONFIG } from "~/utils/config";
---
<!-- Google Analytics -->
{ANALYTICS_CONFIG?.vendors?.googleAnalytics?.isEnabled && <GoogleAnalytics id={String(ANALYTICS_CONFIG.vendors.googleAnalytics)} partytown={true} />}

View File

@ -0,0 +1,33 @@
---
import { UI_CONFIG } from "~/utils/config";
// TODO: This code is temporary
---
<script is:inline define:vars={{ defaultTheme: UI_CONFIG.theme || "system" }}>
function applyTheme(theme) {
if (theme === "dark") {
document.documentElement.classList.add("dark");
} else {
document.documentElement.classList.remove("dark");
}
const matches = document.querySelectorAll("[data-aw-toggle-color-scheme] > input");
if (matches && matches.length) {
matches.forEach((elem) => {
elem.checked = theme !== "dark";
});
}
}
if ((defaultTheme && defaultTheme.endsWith(":only")) || (!localStorage.theme && defaultTheme !== "system")) {
applyTheme(defaultTheme.replace(":only", ""));
} else if (
localStorage.theme === "dark" ||
(!("theme" in localStorage) && window.matchMedia("(prefers-color-scheme: dark)").matches)
) {
applyTheme("dark");
} else {
applyTheme("light");
}
</script>

View File

@ -1,8 +1,8 @@
--- ---
import { SITE } from '~/config.mjs'; import { UI_CONFIG } from '~/utils/config';
--- ---
<script is:inline define:vars={{ defaultTheme: SITE.defaultTheme }}> <script is:inline define:vars={{ defaultTheme: UI_CONFIG.theme }}>
function applyTheme(theme) { function applyTheme(theme) {
if (theme === 'dark') { if (theme === 'dark') {
document.documentElement.classList.add('dark'); document.documentElement.classList.add('dark');

View File

@ -0,0 +1,8 @@
---
import { getAsset } from '~/utils/permalinks';
---
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="sitemap" href={getAsset('/sitemap-index.xml')} />

View File

@ -1,88 +0,0 @@
---
import { AstroSeo } from '@astrolib/seo';
import { GoogleAnalytics } from '@astrolib/analytics';
import { getImage } from '@astrojs/image';
import { SITE } from '~/config.mjs';
import { MetaSEO } from '~/types';
import { getCanonical, getAsset } from '~/utils/permalinks';
import { getRelativeUrlByFilePath } from '~/utils/directories';
export interface Props extends MetaSEO {
dontUseTitleTemplate?: boolean;
}
const defaultImage = SITE.defaultImage
? (
await getImage({
src: SITE.defaultImage,
alt: 'Default image',
width: 1200,
height: 628,
})
).src
: '';
const {
title = SITE.name,
description = '',
image: _image = defaultImage,
canonical = getCanonical(String(Astro.url.pathname)),
noindex = false,
nofollow = false,
ogTitle = title,
ogType = 'website',
dontUseTitleTemplate = false,
} = Astro.props;
const image =
typeof _image === 'string'
? new URL(_image, Astro.site)
: _image && typeof _image['src'] !== 'undefined'
? // @ts-ignore
new URL(getRelativeUrlByFilePath(_image.src), Astro.site)
: null;
---
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<AstroSeo
title={title}
titleTemplate={dontUseTitleTemplate ? '%s' : `%s — ${SITE.name}`}
description={description}
canonical={String(canonical)}
noindex={noindex}
nofollow={nofollow}
openGraph={{
url: String(canonical),
title: ogTitle,
description: description,
type: ogType,
images: image
? [
{
url: image.toString(),
alt: ogTitle,
},
]
: undefined,
// site_name: 'SiteName',
}}
twitter={{
// handle: '@handle',
// site: '@site',
cardType: image ? 'summary_large_image' : undefined,
}}
/>
<!-- Google Site Verification -->
{SITE.googleSiteVerificationId && <meta name="google-site-verification" content={SITE.googleSiteVerificationId} />}
<!-- Google Analytics -->
{SITE.googleAnalyticsId && <GoogleAnalytics id={String(SITE.googleAnalyticsId)} partytown={true} />}
<link rel="sitemap" href={getAsset('/sitemap-index.xml')} />

View File

@ -0,0 +1,68 @@
---
import merge from 'lodash.merge';
import { AstroSeo } from '@astrolib/seo';
import type { AstroSeoProps } from '@astrolib/seo/src/types';
import { SITE_CONFIG, METADATA_CONFIG, I18N_CONFIG } from '~/utils/config';
import { MetaData } from '~/types';
import { getCanonical } from '~/utils/permalinks';
import { adaptOpenGraphImages } from '~/utils/images';
export interface Props extends MetaData {
dontUseTitleTemplate?: boolean;
}
const {
title,
ignoreTitleTemplate = false,
canonical = String(getCanonical(String(Astro.url.pathname))),
robots = {},
description,
openGraph = {},
twitter = {},
} = Astro.props;
const seoProps: AstroSeoProps = merge(
{
title: '',
titleTemplate: '%s',
canonical: canonical,
noindex: true,
nofollow: true,
description: undefined,
openGraph: {
url: canonical,
siteName: SITE_CONFIG?.name,
images: [],
locale: I18N_CONFIG?.language || 'en',
type: 'website',
},
twitter: {
cardType: openGraph?.images?.length ? 'summary_large_image' : 'summary',
},
},
{
title: METADATA_CONFIG?.title?.default,
titleTemplate: METADATA_CONFIG?.title?.template,
noindex: typeof METADATA_CONFIG?.robots?.index !== 'undefined' ? !METADATA_CONFIG.robots.index : undefined,
nofollow: typeof METADATA_CONFIG?.robots?.follow !== 'undefined' ? !METADATA_CONFIG.robots.follow : undefined,
description: METADATA_CONFIG?.description,
openGraph: METADATA_CONFIG?.openGraph,
twitter: METADATA_CONFIG?.twitter,
},
{
title: title,
titleTemplate: ignoreTitleTemplate ? '%s' : undefined,
canonical: canonical,
noindex: typeof robots?.index !== 'undefined' ? !robots.index : undefined,
nofollow: typeof robots?.follow !== 'undefined' ? !robots.follow : undefined,
description: description,
openGraph: { url: canonical, ...openGraph },
twitter: twitter,
}
);
---
<AstroSeo {...{ ...seoProps, openGraph: await adaptOpenGraphImages(seoProps?.openGraph, Astro.site) }} />

View File

@ -0,0 +1,5 @@
---
import { SITE_CONFIG } from "~/utils/config";
---
{SITE_CONFIG.googleSiteVerificationId && <meta name="google-site-verification" content={SITE_CONFIG.googleSiteVerificationId} />}

View File

@ -1,7 +1,7 @@
--- ---
import { Icon } from 'astro-icon/components'; import { Icon } from 'astro-icon/components';
import { SITE } from '~/config.mjs'; import { UI_CONFIG } from '~/utils/config';
export interface Props { export interface Props {
label?: string; label?: string;
@ -20,7 +20,7 @@ const {
--- ---
{ {
!(SITE?.defaultTheme && SITE.defaultTheme.endsWith(':only')) && ( !(UI_CONFIG.theme && UI_CONFIG.theme.endsWith(':only')) && (
<button type="button" class={className} aria-label={label} data-aw-toggle-color-scheme> <button type="button" class={className} aria-label={label} data-aw-toggle-color-scheme>
<Icon name={iconName} class={iconClass} /> <Icon name={iconName} class={iconClass} />
</button> </button>

View File

@ -0,0 +1,64 @@
---
import { APP_BLOG_CONFIG } from "~/utils/config";
import Grid from "~/components/blog/Grid.astro";
import { getBlogPermalink } from "~/utils/permalinks";
import { findPostsByIds } from "~/utils/blog";
import WidgetWrapper from "~/components/ui/WidgetWrapper.astro";
import type { Widget } from "~/types";
export interface Props extends Widget {
title?: string;
linkText?: string;
linkUrl?: string | URL;
information?: string;
postIds: string[];
}
const {
title = await Astro.slots.render("title"),
linkText = "View all posts",
linkUrl = getBlogPermalink(),
information = await Astro.slots.render("information"),
postIds = [],
id,
isDark = false,
classes = {},
bg = await Astro.slots.render("bg"),
} = Astro.props;
const posts = APP_BLOG_CONFIG.isEnabled ? await findPostsByIds(postIds) : [];
---
{
APP_BLOG_CONFIG.isEnabled ? (
<WidgetWrapper id={id} isDark={isDark} containerClass={classes?.container} bg={bg}>
<div class="flex flex-col lg:justify-between lg:flex-row mb-8">
{title && (
<div class="md:max-w-sm">
<h2
class="text-3xl font-bold tracking-tight sm:text-4xl sm:leading-none group font-heading mb-2"
set:html={title}
/>
{APP_BLOG_CONFIG.list.isEnabled && linkText && linkUrl && (
<a
class="text-muted dark:text-slate-400 hover:text-primary transition ease-in duration-200 block mb-6 lg:mb-0"
href={linkUrl}
>
{linkText} »
</a>
)}
</div>
)}
{information && <p class="text-muted dark:text-slate-400 lg:text-sm lg:max-w-md" set:html={information} />}
</div>
<Grid posts={posts} />
</WidgetWrapper>
) : (
<Fragment />
)
}

View File

@ -0,0 +1,64 @@
---
import { APP_BLOG_CONFIG } from "~/utils/config";
import Grid from "~/components/blog/Grid.astro";
import { getBlogPermalink } from "~/utils/permalinks";
import { findLatestPosts } from "~/utils/blog";
import WidgetWrapper from "~/components/ui/WidgetWrapper.astro";
import type { Widget } from "~/types";
export interface Props extends Widget {
title?: string;
linkText?: string;
linkUrl?: string | URL;
information?: string;
count?: number;
}
const {
title = await Astro.slots.render("title"),
linkText = "View all posts",
linkUrl = getBlogPermalink(),
information = await Astro.slots.render("information"),
count = 4,
id,
isDark = false,
classes = {},
bg = await Astro.slots.render("bg"),
} = Astro.props;
const posts = APP_BLOG_CONFIG.isEnabled ? await findLatestPosts({ count }) : [];
---
{
APP_BLOG_CONFIG.isEnabled ? (
<WidgetWrapper id={id} isDark={isDark} containerClass={classes?.container} bg={bg}>
<div class="flex flex-col lg:justify-between lg:flex-row mb-8">
{title && (
<div class="md:max-w-sm">
<h2
class="text-3xl font-bold tracking-tight sm:text-4xl sm:leading-none group font-heading mb-2"
set:html={title}
/>
{APP_BLOG_CONFIG.list.isEnabled && linkText && linkUrl && (
<a
class="text-muted dark:text-slate-400 hover:text-primary transition ease-in duration-200 block mb-6 lg:mb-0"
href={linkUrl}
>
{linkText} »
</a>
)}
</div>
)}
{information && <p class="text-muted dark:text-slate-400 lg:text-sm lg:max-w-md" set:html={information} />}
</div>
<Grid posts={posts} />
</WidgetWrapper>
) : (
<Fragment />
)
}

View File

@ -31,7 +31,7 @@ const {
} = Astro.props; } = Astro.props;
--- ---
<section class:list={[{ 'pt-0 md:pt-0': isAfterContent }, 'bg-blue-50 dark:bg-slate-800 py-16 md:py-20 not-prose']}> <section class:list={[{ 'pt-0 md:pt-0': isAfterContent }, 'bg-blue-50 dark:bg-page py-16 md:py-20 not-prose']}>
<div class="max-w-xl sm:mx-auto lg:max-w-2xl"> <div class="max-w-xl sm:mx-auto lg:max-w-2xl">
{ {
(title || subtitle || tagline) && ( (title || subtitle || tagline) && (

View File

@ -1,6 +1,6 @@
--- ---
import { Icon } from 'astro-icon/components'; import { Icon } from 'astro-icon/components';
import { SITE } from '~/config.mjs'; import { SITE_CONFIG } from '~/utils/config';
import { getHomePermalink } from '~/utils/permalinks'; import { getHomePermalink } from '~/utils/permalinks';
interface Link { interface Link {
@ -32,7 +32,7 @@ const { socialLinks = [], secondaryLinks = [], links = [], footNote = '', theme
<div class="grid grid-cols-12 gap-4 gap-y-8 sm:gap-8 py-8 md:py-12"> <div class="grid grid-cols-12 gap-4 gap-y-8 sm:gap-8 py-8 md:py-12">
<div class="col-span-12 lg:col-span-4"> <div class="col-span-12 lg:col-span-4">
<div class="mb-2"> <div class="mb-2">
<a class="inline-block font-bold text-xl" href={getHomePermalink()}>{SITE?.name}</a> <a class="inline-block font-bold text-xl" href={getHomePermalink()}>{SITE_CONFIG?.name}</a>
</div> </div>
<div class="text-sm text-muted"> <div class="text-sm text-muted">
{ {

View File

@ -44,7 +44,8 @@ const {
aspectRatio="432:768" aspectRatio="432:768"
width={432} width={432}
height={768} height={768}
{...image} src={image?.src}
alt={image?.alt || ""}
/> />
)) ))
} }

View File

@ -1,62 +0,0 @@
import defaultImage from './assets/images/default.png';
const CONFIG = {
name: 'AstroWind',
origin: 'https://astrowind.vercel.app',
basePathname: '/',
trailingSlash: false,
title: 'AstroWind — Free template for create a website with Astro + Tailwind CSS',
description:
'🚀 Suitable for Startups, Small Business, Sass Websites, Professional Portfolios, Marketing Websites, Landing Pages & Blogs.',
defaultImage: defaultImage,
defaultTheme: 'system', // Values: "system" | "light" | "dark" | "light:only" | "dark:only"
language: 'en',
textDirection: 'ltr',
dateFormatter: new Intl.DateTimeFormat('en', {
year: 'numeric',
month: 'short',
day: 'numeric',
timeZone: 'UTC',
}),
googleAnalyticsId: false, // or "G-XXXXXXXXXX",
googleSiteVerificationId: 'orcPxI47GSa-cRvY11tUe6iGg2IO_RPvnA1q95iEM3M',
blog: {
disabled: false,
postsPerPage: 4,
post: {
permalink: '/%slug%', // Variables: %slug%, %year%, %month%, %day%, %hour%, %minute%, %second%, %category%
noindex: false,
disabled: false,
},
list: {
pathname: 'blog', // Blog main path, you can change this to "articles" (/articles)
noindex: false,
disabled: false,
},
category: {
pathname: 'category', // Category main path /category/some-category
noindex: true,
disabled: false,
},
tag: {
pathname: 'tag', // Tag main path /tag/some-tag
noindex: true,
disabled: false,
},
},
};
export const SITE = { ...CONFIG, blog: undefined };
export const BLOG = CONFIG.blog;
export const DATE_FORMATTER = CONFIG.dateFormatter;

View File

@ -1,23 +1,68 @@
import { z, defineCollection } from 'astro:content'; import { z, defineCollection } from 'astro:content';
const post = defineCollection({ const metadataDefinition = () =>
schema: z.object({ z
title: z.string(), .object({
description: z.string().optional(), title: z.string().optional(),
image: z.string().optional(), ignoreTitleTemplate: z.boolean().optional(),
canonical: z.string().url().optional(), canonical: z.string().url().optional(),
publishDate: z.date().or(z.string()).optional(), robots: z
.object({
index: z.boolean().optional(),
follow: z.boolean().optional(),
})
.optional(),
description: z.string().optional(),
openGraph: z
.object({
url: z.string().optional(),
siteName: z.string().optional(),
images: z
.array(
z.object({
url: z.string(),
width: z.number().optional(),
height: z.number().optional(),
})
)
.optional(),
locale: z.string().optional(),
type: z.string().optional(),
})
.optional(),
twitter: z
.object({
handle: z.string().optional(),
site: z.string().optional(),
cardType: z.string().optional(),
})
.optional(),
})
.optional();
const postCollection = defineCollection({
schema: z.object({
publishDate: z.date().optional(),
updateDate: z.date().optional(),
draft: z.boolean().optional(), draft: z.boolean().optional(),
title: z.string(),
excerpt: z.string().optional(), excerpt: z.string().optional(),
image: z.string().optional(),
category: z.string().optional(), category: z.string().optional(),
tags: z.array(z.string()).optional(), tags: z.array(z.string()).optional(),
author: z.string().optional(), author: z.string().optional(),
metadata: metadataDefinition(),
}), }),
}); });
export const collections = { export const collections = {
post: post, post: postCollection,
}; };

View File

@ -1,7 +1,6 @@
--- ---
publishDate: 2023-07-17T00:00:00Z publishDate: 2023-07-17T00:00:00Z
title: AstroWind template in depth title: AstroWind template in depth
description: Internals documentation
excerpt: While easy to get started, Astrowind is quite complex internally. This page provides documentation on some of the more intricate parts. 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: ~/assets/images/stickers.jpg
category: Documentation category: Documentation
@ -9,6 +8,7 @@ tags:
- astro - astro
- tailwind css - tailwind css
- front-end - front-end
metadata:
canonical: https://astrowind.vercel.app/astrowind-template-in-depth canonical: https://astrowind.vercel.app/astrowind-template-in-depth
--- ---

View File

@ -1,13 +1,13 @@
--- ---
publishDate: 2023-01-12T00:00:00Z publishDate: 2023-01-12T00:00:00Z
title: Get started with AstroWind to create a website using Astro and Tailwind CSS title: Get started with AstroWind to create a website using Astro and Tailwind CSS
description: Lorem ipsum dolor sit amet
excerpt: Sint sit cillum pariatur eiusmod nulla pariatur ipsum. Sit laborum anim qui mollit tempor pariatur. excerpt: Sint sit cillum pariatur eiusmod nulla pariatur ipsum. Sit laborum anim qui mollit tempor pariatur.
image: ~/assets/images/do-more.jpg image: ~/assets/images/do-more.jpg
category: Tutorials category: Tutorials
tags: tags:
- astro - astro
- tailwind css - tailwind css
metadata:
canonical: https://astrowind.vercel.app/get-started-website-with-astro-tailwind-css canonical: https://astrowind.vercel.app/get-started-website-with-astro-tailwind-css
--- ---

View File

@ -7,6 +7,7 @@ tags:
- astro - astro
- tailwind css - tailwind css
- theme - theme
metadata:
canonical: https://astrowind.vercel.app/how-to-customize-astrowind-to-your-brand canonical: https://astrowind.vercel.app/how-to-customize-astrowind-to-your-brand
--- ---

View File

@ -1,7 +1,6 @@
--- ---
publishDate: 2023-01-02T00:00:00Z publishDate: 2023-01-02T00:00:00Z
title: Markdown elements demo post title: Markdown elements demo post
description: Lorem ipsum dolor sit amet
excerpt: Sint sit cillum pariatur eiusmod nulla pariatur ipsum. Sit laborum anim qui mollit tempor pariatur nisi minim dolor. excerpt: Sint sit cillum pariatur eiusmod nulla pariatur ipsum. Sit laborum anim qui mollit tempor pariatur nisi minim dolor.
tags: tags:
- markdown - markdown

1
src/env.d.ts vendored
View File

@ -1,2 +1,3 @@
// eslint-disable-next-line @typescript-eslint/triple-slash-reference
/// <reference path="../.astro/types.d.ts" /> /// <reference path="../.astro/types.d.ts" />
/// <reference types="@astrojs/image/client" /> /// <reference types="@astrojs/image/client" />

View File

@ -1,37 +0,0 @@
---
import '~/assets/styles/tailwind.css';
import MetaTags from '~/components/common/MetaTags.astro';
import Favicons from '~/components/Favicons.astro';
import CustomStyles from "~/components/CustomStyles.astro"
import BasicScripts from '~/components/common/BasicScripts.astro';
import { MetaSEO } from '~/types';
import { SITE } from '~/config.mjs';
export interface Props {
meta?: MetaSEO;
}
const { meta = {} } = Astro.props;
const { language = 'en', textDirection = 'ltr' } = SITE;
---
<!DOCTYPE html>
<html lang={language} dir={textDirection} class="2xl:text-[20px]">
<head>
<Favicons />
<CustomStyles />
<MetaTags {...meta} />
</head>
<body class="antialiased text-page bg-light dark:text-slate-300 tracking-tight dark:bg-dark">
<slot />
<BasicScripts />
<style is:global>
img {
content-visibility: auto;
}
</style>
</body>
</html>

49
src/layouts/Layout.astro Normal file
View File

@ -0,0 +1,49 @@
---
import '~/assets/styles/tailwind.css';
import { I18N_CONFIG } from "~/utils/config";
import CommonMeta from '~/components/common/CommonMeta.astro';
import Favicons from '~/components/Favicons.astro';
import CustomStyles from "~/components/CustomStyles.astro"
import ApplyColorMode from "~/components/common/ApplyColorMode.astro"
import Metadata from '~/components/common/Metadata.astro';
import SiteVerification from "~/components/common/SiteVerification.astro"
import Analytics from "~/components/common/Analytics.astro"
import BasicScripts from '~/components/common/BasicScripts.astro';
import { MetaData as MetaDataType } from '~/types';
export interface Props {
metadata?: MetaDataType;
}
const { metadata = {} } = Astro.props;
const { language, textDirection } = I18N_CONFIG;
---
<!DOCTYPE html>
<html lang={language} dir={textDirection} class="2xl:text-[20px]">
<head>
<CommonMeta />
<Favicons />
<CustomStyles />
<ApplyColorMode />
<Metadata {...metadata} />
<SiteVerification />
<Analytics />
</head>
<body class="antialiased text-default bg-page tracking-tight">
<slot />
<BasicScripts />
<style is:global>
img {
content-visibility: auto;
}
</style>
</body>
</html>

View File

@ -1,7 +1,7 @@
--- ---
import Layout from '~/layouts/PageLayout.astro'; import Layout from '~/layouts/PageLayout.astro';
import { MetaSEO } from '~/types'; import { MetaData } from '~/types';
export interface Props { export interface Props {
frontmatter: { frontmatter: {
@ -11,12 +11,12 @@ export interface Props {
const { frontmatter } = Astro.props; const { frontmatter } = Astro.props;
const meta: MetaSEO = { const metadata: MetaData = {
title: frontmatter?.title, title: frontmatter?.title,
}; };
--- ---
<Layout {meta}> <Layout metadata={metadata}>
<section class="px-4 py-16 sm:px-6 mx-auto lg:px-8 lg:py-20 max-w-4xl"> <section class="px-4 py-16 sm:px-6 mx-auto lg:px-8 lg:py-20 max-w-4xl">
<h1 class="font-bold font-heading text-4xl md:text-5xl leading-tighter tracking-tighter">{frontmatter.title}</h1> <h1 class="font-bold font-heading text-4xl md:text-5xl leading-tighter tracking-tighter">{frontmatter.title}</h1>
<div <div

View File

@ -1,21 +1,21 @@
--- ---
import Layout from '~/layouts/BaseLayout.astro'; import Layout from '~/layouts/Layout.astro';
import Header from '~/components/widgets/Header.astro'; import Header from '~/components/widgets/Header.astro';
import Footer from '~/components/widgets/Footer.astro'; import Footer from '~/components/widgets/Footer.astro';
import Announcement from '~/components/widgets/Announcement.astro'; import Announcement from '~/components/widgets/Announcement.astro';
import { headerData, footerData } from '~/navigation'; import { headerData, footerData } from '~/navigation';
import { MetaSEO } from '~/types'; import { MetaData } from '~/types';
export interface Props { export interface Props {
meta?: MetaSEO; metadata?: MetaData;
} }
const { meta } = Astro.props; const { metadata } = Astro.props;
--- ---
<Layout {meta}> <Layout metadata={metadata}>
<slot name="announcement"> <slot name="announcement">
<Announcement /> <Announcement />
</slot> </slot>

View File

@ -1,11 +1,11 @@
--- ---
import Layout from '~/layouts/BaseLayout.astro'; import Layout from '~/layouts/Layout.astro';
import { getHomePermalink } from '~/utils/permalinks'; import { getHomePermalink } from '~/utils/permalinks';
const title = `Error 404`; const title = `Error 404`;
--- ---
<Layout meta={{ title }}> <Layout metadata={{ title }}>
<section class="flex items-center h-full p-16"> <section class="flex items-center h-full p-16">
<div class="container flex flex-col items-center justify-center px-5 mx-auto my-8"> <div class="container flex flex-col items-center justify-center px-5 mx-auto my-8">
<div class="max-w-md text-center"> <div class="max-w-md text-center">

View File

@ -1,23 +1,13 @@
--- ---
import { SITE, BLOG } from '~/config.mjs';
import Layout from '~/layouts/PageLayout.astro'; import Layout from '~/layouts/PageLayout.astro';
import BlogList from '~/components/blog/List.astro'; import BlogList from '~/components/blog/List.astro';
import Headline from '~/components/blog/Headline.astro'; import Headline from '~/components/blog/Headline.astro';
import Pagination from '~/components/blog/Pagination.astro'; import Pagination from '~/components/blog/Pagination.astro';
// import PostTags from "~/components/blog/Tags.astro"; // import PostTags from "~/components/blog/Tags.astro";
import { fetchPosts } from '~/utils/blog'; import { blogListRobots, getStaticPathsBlogList } from '~/utils/blog';
// import { findTags, findCategories } from '~/utils/blog';
import { BLOG_BASE } from '~/utils/permalinks';
export async function getStaticPaths({ paginate }) { export const getStaticPaths = getStaticPathsBlogList();
if (BLOG?.disabled || BLOG?.list?.disabled) return [];
return paginate(await fetchPosts(), {
params: { blog: BLOG_BASE || undefined },
pageSize: BLOG.postsPerPage,
});
}
const { page } = Astro.props; const { page } = Astro.props;
const currentPage = page.currentPage ?? 1; const currentPage = page.currentPage ?? 1;
@ -25,15 +15,19 @@ const currentPage = page.currentPage ?? 1;
// const allCategories = await findCategories(); // const allCategories = await findCategories();
// const allTags = await findTags(); // const allTags = await findTags();
const meta = { const metadata = {
title: `Blog${currentPage > 1 ? ` — Page ${currentPage}` : ''}`, title: `Blog${currentPage > 1 ? ` — Page ${currentPage}` : ''}`,
description: SITE.description, robots: {
noindex: BLOG?.list?.noindex || currentPage > 1, index: blogListRobots?.index && currentPage === 1,
ogType: 'blog', follow: blogListRobots?.follow,
},
openGraph: {
type: 'blog',
},
}; };
--- ---
<Layout {meta}> <Layout metadata={metadata}>
<section class="px-6 sm:px-6 py-12 sm:py-16 lg:py-20 mx-auto max-w-4xl"> <section class="px-6 sm:px-6 py-12 sm:py-16 lg:py-20 mx-auto max-w-4xl">
<Headline <Headline
subtitle="A statically generated blog example with news, tutorials, resources and other interesting content related to AstroWind" subtitle="A statically generated blog example with news, tutorials, resources and other interesting content related to AstroWind"

View File

@ -1,47 +1,28 @@
--- ---
import { SITE, BLOG } from '~/config.mjs'; import { blogCategoryRobots, getStaticPathsBlogCategory } from '~/utils/blog';
import Layout from '~/layouts/PageLayout.astro'; import Layout from '~/layouts/PageLayout.astro';
import BlogList from '~/components/blog/List.astro'; import BlogList from '~/components/blog/List.astro';
import Headline from '~/components/blog/Headline.astro'; import Headline from '~/components/blog/Headline.astro';
import Pagination from '~/components/blog/Pagination.astro'; import Pagination from '~/components/blog/Pagination.astro';
import { fetchPosts } from '~/utils/blog'; export const getStaticPaths = getStaticPathsBlogCategory();
import { CATEGORY_BASE } from '~/utils/permalinks';
export async function getStaticPaths({ paginate }) {
if (BLOG?.disabled || BLOG?.category?.disabled) return [];
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: BLOG.postsPerPage,
props: { category },
}
)
);
}
const { page, category } = Astro.props; const { page, category } = Astro.props;
const currentPage = page.currentPage ?? 1; const currentPage = page.currentPage ?? 1;
const meta = {
const metadata = {
title: `Category '${category}' ${currentPage > 1 ? ` — Page ${currentPage}` : ''}`, title: `Category '${category}' ${currentPage > 1 ? ` — Page ${currentPage}` : ''}`,
description: SITE.description, robots: {
noindex: BLOG?.category?.noindex, index: blogCategoryRobots?.index,
follow: blogCategoryRobots?.follow,
},
}; };
--- ---
<Layout meta={meta}> <Layout metadata={metadata}>
<section class="px-6 sm:px-6 py-12 sm:py-16 lg:py-20 mx-auto max-w-4xl"> <section class="px-4 md:px-6 py-12 sm:py-16 lg:py-20 mx-auto max-w-4xl">
<Headline><span class="capitalize">{category.replaceAll('-', ' ')}</span></Headline> <Headline><span class="capitalize">{category.replaceAll('-', ' ')}</span></Headline>
<BlogList posts={page.data} /> <BlogList posts={page.data} />
<Pagination prevUrl={page.url.prev} nextUrl={page.url.next} /> <Pagination prevUrl={page.url.prev} nextUrl={page.url.next} />

View File

@ -1,47 +1,28 @@
--- ---
import { SITE, BLOG } from '~/config.mjs'; import { blogTagRobots, getStaticPathsBlogTag } from '~/utils/blog';
import Layout from '~/layouts/PageLayout.astro'; import Layout from '~/layouts/PageLayout.astro';
import BlogList from '~/components/blog/List.astro'; import BlogList from '~/components/blog/List.astro';
import Headline from '~/components/blog/Headline.astro';
import Pagination from '~/components/blog/Pagination.astro'; import Pagination from '~/components/blog/Pagination.astro';
import { fetchPosts } from '~/utils/blog'; export const getStaticPaths = getStaticPathsBlogTag();
import { TAG_BASE } from '~/utils/permalinks';
import Headline from '~/components/blog/Headline.astro';
export async function getStaticPaths({ paginate }) {
if (BLOG?.disabled || BLOG?.tag?.disabled) return [];
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: BLOG.postsPerPage,
props: { tag },
}
)
);
}
const { page, tag } = Astro.props; const { page, tag } = Astro.props;
const currentPage = page.currentPage ?? 1; const currentPage = page.currentPage ?? 1;
const meta = {
const metadata = {
title: `Posts by tag '${tag}'${currentPage > 1 ? ` — Page ${currentPage} ` : ''}`, title: `Posts by tag '${tag}'${currentPage > 1 ? ` — Page ${currentPage} ` : ''}`,
description: SITE.description, robots: {
noindex: BLOG?.tag?.noindex, index: blogTagRobots?.index,
follow: blogTagRobots?.follow,
},
}; };
--- ---
<Layout meta={meta}> <Layout metadata={metadata}>
<section class="px-6 sm:px-6 py-12 sm:py-16 lg:py-20 mx-auto max-w-4xl"> <section class="px-4 md:px-6 py-12 sm:py-16 lg:py-20 mx-auto max-w-4xl">
<Headline>Tag: {tag}</Headline> <Headline>Tag: {tag}</Headline>
<BlogList posts={page.data} /> <BlogList posts={page.data} />
<Pagination prevUrl={page.url.prev} nextUrl={page.url.next} /> <Pagination prevUrl={page.url.prev} nextUrl={page.url.next} />

View File

@ -1,38 +1,39 @@
--- ---
import { BLOG } from '~/config.mjs'; import merge from 'lodash.merge';
import type { ImageMetadata } from 'astro';
import Layout from '~/layouts/PageLayout.astro'; import Layout from '~/layouts/PageLayout.astro';
import SinglePost from '~/components/blog/SinglePost.astro'; import SinglePost from '~/components/blog/SinglePost.astro';
import ToBlogLink from '~/components/blog/ToBlogLink.astro'; import ToBlogLink from '~/components/blog/ToBlogLink.astro';
import { getCanonical, getPermalink } from '~/utils/permalinks'; import { getCanonical, getPermalink } from '~/utils/permalinks';
import { fetchPosts } from '~/utils/blog'; import { getStaticPathsBlogPost, blogPostRobots } from '~/utils/blog';
import { findImage } from '~/utils/images'; import { findImage } from '~/utils/images';
export async function getStaticPaths() { export const getStaticPaths = getStaticPathsBlogPost();
if (BLOG?.disabled || BLOG?.post?.disabled) return [];
return (await fetchPosts()).map((post) => ({
params: {
blog: post.permalink,
},
props: { post },
}));
}
const { post } = Astro.props; const { post } = Astro.props;
const url = getCanonical(getPermalink(post.permalink, 'post'));
const meta = { const url = getCanonical(getPermalink(post.permalink, 'post'));
const image = (await findImage(post.image)) as ImageMetadata | undefined;
const metadata = merge(
{
title: post.title, title: post.title,
description: post.description, description: post.excerpt,
canonical: post.canonical || url, robots: {
image: await findImage(post.image), index: blogPostRobots?.index,
noindex: BLOG?.post?.noindex, follow: blogPostRobots?.follow,
ogType: 'article', },
}; openGraph: {
type: 'article',
...(image ? { images: [{ url: image?.src, width: image?.width, height: image?.height }] } : {}),
},
},
{ ...(post?.metadata ? { ...post.metadata, canonical: post.metadata?.canonical || url } : {}) }
);
--- ---
<Layout {meta}> <Layout metadata={metadata}>
<SinglePost post={{ ...post, image: meta.image }} url={url} /> <SinglePost post={{ ...post, image: image }} url={url} />
<ToBlogLink /> <ToBlogLink />
</Layout> </Layout>

View File

@ -1,11 +1,11 @@
--- ---
import Layout from '~/layouts/PageLayout.astro'; import Layout from '~/layouts/PageLayout.astro';
const meta = { const metadata = {
title: "About us", title: "About us",
}; };
--- ---
<Layout {meta}> <Layout metadata={metadata}>
About us About us
</Layout> </Layout>

View File

@ -1,11 +1,11 @@
--- ---
import Layout from '~/layouts/PageLayout.astro'; import Layout from '~/layouts/PageLayout.astro';
const meta = { const metadata = {
title: "Contact", title: "Contact",
}; };
--- ---
<Layout {meta}> <Layout metadata={metadata}>
Contact Contact
</Layout> </Layout>

View File

@ -1,5 +1,4 @@
--- ---
import { SITE } from '~/config.mjs';
import Layout from '~/layouts/PageLayout.astro'; import Layout from '~/layouts/PageLayout.astro';
import Hero from '~/components/widgets/Hero.astro'; import Hero from '~/components/widgets/Hero.astro';
@ -8,19 +7,18 @@ import Features from '~/components/widgets/Features.astro';
import Features2 from '~/components/widgets/Features2.astro'; import Features2 from '~/components/widgets/Features2.astro';
import Steps from '~/components/widgets/Steps.astro'; import Steps from '~/components/widgets/Steps.astro';
import Content from '~/components/widgets/Content.astro'; import Content from '~/components/widgets/Content.astro';
import LatestPosts from '~/components/blog/LatestPosts.astro'; import BlogLatestPosts from '~/components/widgets/BlogLatestPosts.astro';
import FAQs from '~/components/widgets/FAQs.astro'; import FAQs from '~/components/widgets/FAQs.astro';
import Stats from '~/components/widgets/Stats.astro'; import Stats from '~/components/widgets/Stats.astro';
import CallToAction from '~/components/widgets/CallToAction.astro'; import CallToAction from '~/components/widgets/CallToAction.astro';
const meta = { const metadata = {
title: SITE.title, title: "AstroWind — Free template for create a website with Astro + Tailwind CSS",
description: SITE.description,
dontUseTitleTemplate: true, dontUseTitleTemplate: true,
}; };
--- ---
<Layout {meta}> <Layout metadata={metadata}>
<!-- Hero Widget ******************* --> <!-- Hero Widget ******************* -->
<Hero <Hero
@ -262,7 +260,7 @@ const meta = {
<!-- HighlightedPosts Widget ******* --> <!-- HighlightedPosts Widget ******* -->
<LatestPosts <BlogLatestPosts
title="Find out more content in our Blog" title="Find out more content in our Blog"
information={`The blog is used to display AstroWind documentation. information={`The blog is used to display AstroWind documentation.
Each new article will be an important step that you will need to know to be an expert in creating a website using Astro + Tailwind CSS. Each new article will be an important step that you will need to know to be an expert in creating a website using Astro + Tailwind CSS.

View File

@ -5,12 +5,12 @@ import Header from '~/components/widgets/Header.astro';
import Hero2 from '~/components/widgets/Hero2.astro'; import Hero2 from '~/components/widgets/Hero2.astro';
import CallToAction from '~/components/widgets/CallToAction.astro'; import CallToAction from '~/components/widgets/CallToAction.astro';
const meta = { const metadata = {
title: "Mobile App Landing Page", title: "Mobile App Landing Page",
}; };
--- ---
<Layout {meta}> <Layout metadata={metadata}>
<Fragment slot="announcement"></Fragment> <Fragment slot="announcement"></Fragment>
<Fragment slot="header"> <Fragment slot="header">
<Header <Header

View File

@ -9,12 +9,12 @@ import CallToAction from '~/components/widgets/CallToAction.astro';
import { headerData } from '~/navigation'; import { headerData } from '~/navigation';
const meta = { const metadata = {
title: 'Saas Landing Page', title: 'Saas Landing Page',
}; };
--- ---
<Layout {meta}> <Layout metadata={metadata}>
<Fragment slot="header"> <Fragment slot="header">
<Header <Header
{...headerData} {...headerData}

View File

@ -7,12 +7,12 @@ import CallToAction from '~/components/widgets/CallToAction.astro';
import { headerData } from '~/navigation'; import { headerData } from '~/navigation';
const meta = { const metadata = {
title: "Startup Landing Page", title: "Startup Landing Page",
}; };
--- ---
<Layout {meta}> <Layout metadata={metadata}>
<Fragment slot="header"> <Fragment slot="header">
<Header <Header
{...headerData} {...headerData}

View File

@ -3,12 +3,12 @@ import Layout from '~/layouts/PageLayout.astro';
import Pricing from '~/components/widgets/Pricing.astro'; import Pricing from '~/components/widgets/Pricing.astro';
const meta = { const metadata = {
title: "Pricing", title: "Pricing",
}; };
--- ---
<Layout {meta}> <Layout metadata={metadata}>
<Pricing <Pricing
title="Basic Pricing" title="Basic Pricing"
tagline="Pricing" tagline="Pricing"

View File

@ -1,11 +1,11 @@
import rss from '@astrojs/rss'; import rss from '@astrojs/rss';
import { SITE, BLOG } from '~/config.mjs'; import { SITE_CONFIG, METADATA_CONFIG, APP_BLOG_CONFIG } from '~/utils/config';
import { fetchPosts } from '~/utils/blog'; import { fetchPosts } from '~/utils/blog';
import { getPermalink } from '~/utils/permalinks'; import { getPermalink } from '~/utils/permalinks';
export const get = async () => { export const get = async () => {
if (BLOG.disabled) { if (!APP_BLOG_CONFIG.isEnabled) {
return new Response(null, { return new Response(null, {
status: 404, status: 404,
statusText: 'Not found', statusText: 'Not found',
@ -15,17 +15,17 @@ export const get = async () => {
const posts = await fetchPosts(); const posts = await fetchPosts();
return rss({ return rss({
title: `${SITE.name}s Blog`, title: `${SITE_CONFIG.name}s Blog`,
description: SITE.description, description: METADATA_CONFIG?.description,
site: import.meta.env.SITE, site: import.meta.env.SITE,
items: posts.map((post) => ({ items: posts.map((post) => ({
link: getPermalink(post.permalink, 'post'), link: getPermalink(post.permalink, 'post'),
title: post.title, title: post.title,
description: post.description, description: post.excerpt,
pubDate: post.publishDate, pubDate: post.publishDate,
})), })),
trailingSlash: SITE.trailingSlash, trailingSlash: SITE_CONFIG.trailingSlash,
}); });
}; };

View File

@ -1,11 +1,11 @@
--- ---
import Layout from '~/layouts/PageLayout.astro'; import Layout from '~/layouts/PageLayout.astro';
const meta = { const metadata = {
title: "Services", title: "Services",
}; };
--- ---
<Layout {meta}> <Layout metadata={metadata}>
Services Services
</Layout> </Layout>

81
src/types.d.ts vendored
View File

@ -1,42 +1,83 @@
import { AstroComponentFactory } from 'astro/dist/runtime/server';
export interface Post { export interface Post {
/** A unique ID number that identifies a post. */
id: string; id: string;
/** A posts unique slug part of the posts URL based on its name, i.e. a post called “My Sample Page” has a slug “my-sample-page”. */
slug: string; slug: string;
publishDate: Date; /** */
title: string; permalink: string;
description?: string;
/** */
publishDate: Date;
/** */
updateDate?: Date;
/** */
title: string;
/** Optional summary of post content. */
excerpt?: string;
/** */
image?: string; image?: string;
canonical?: string | URL; /** */
permalink?: string;
draft?: boolean;
excerpt?: string;
category?: string; category?: string;
/** */
tags?: Array<string>; tags?: Array<string>;
/** */
author?: string; author?: string;
Content: AstroComponentFactory; /** */
metadata?: MetaData;
/** */
draft?: boolean;
/** */
Content?: unknown;
content?: string; content?: string;
/** */
readingTime?: number; readingTime?: number;
} }
export interface MetaSEO { export interface MetaData {
title?: string; title?: string;
ignoreTitleTemplate?: boolean;
canonical?: string;
robots?: MetaDataRobots;
description?: string; description?: string;
image?: string;
canonical?: string | URL; openGraph?: MetaDataOpenGraph;
noindex?: boolean; twitter?: MetaDataTwitter;
nofollow?: boolean; }
ogTitle?: string; export interface MetaDataRobots {
ogType?: string; index?: boolean;
follow?: boolean;
}
export interface MetaDataImage {
url: string;
width?: number;
height?: number;
}
export interface MetaDataOpenGraph {
url?: string;
siteName?: string;
images?: Array<MetaDataImage>;
locale?: string;
type?: string;
}
export interface MetaDataTwitter {
handle?: string;
site?: string;
cardType?: string;
} }
export interface Image { export interface Image {
@ -189,7 +230,7 @@ export interface Steps extends Headline, Widget {
icon?: string; icon?: string;
classes?: Record<string, string>; classes?: Record<string, string>;
}>; }>;
image?: string | any; // TODO: find HTMLElementProps image?: string | Image;
isReversed?: boolean; isReversed?: boolean;
} }

View File

@ -1,9 +1,20 @@
import { getCollection } from 'astro:content'; import { getCollection } from 'astro:content';
import type { CollectionEntry } from 'astro:content'; import type { CollectionEntry } from 'astro:content';
import type { Post } from '~/types'; import type { Post } from '~/types';
import { cleanSlug, trimSlash, POST_PERMALINK_PATTERN } from './permalinks'; import { APP_BLOG_CONFIG } from '~/utils/config';
import { cleanSlug, trimSlash, BLOG_BASE, POST_PERMALINK_PATTERN, CATEGORY_BASE, TAG_BASE } from './permalinks';
const generatePermalink = async ({ id, slug, publishDate, category }) => { const generatePermalink = async ({
id,
slug,
publishDate,
category,
}: {
id: string;
slug: string;
publishDate: Date;
category: string | undefined;
}) => {
const year = String(publishDate.getFullYear()).padStart(4, '0'); const year = String(publishDate.getFullYear()).padStart(4, '0');
const month = String(publishDate.getMonth() + 1).padStart(2, '0'); const month = String(publishDate.getMonth() + 1).padStart(2, '0');
const day = String(publishDate.getDate()).padStart(2, '0'); const day = String(publishDate.getDate()).padStart(2, '0');
@ -33,33 +44,46 @@ const getNormalizedPost = async (post: CollectionEntry<'post'>): Promise<Post> =
const { Content, remarkPluginFrontmatter } = await post.render(); const { Content, remarkPluginFrontmatter } = await post.render();
const { const {
publishDate: rawPublishDate = new Date(),
updateDate: rawUpdateDate,
title,
excerpt,
image,
tags: rawTags = [], tags: rawTags = [],
category: rawCategory, category: rawCategory,
author = 'Anonymous', author,
publishDate: rawPublishDate = new Date(), draft = false,
...rest metadata = {},
} = data; } = data;
const slug = cleanSlug(rawSlug.split('/').pop()); const slug = cleanSlug(rawSlug.split('/').pop());
const publishDate = new Date(rawPublishDate); const publishDate = new Date(rawPublishDate);
const updateDate = rawUpdateDate ? new Date(rawUpdateDate) : undefined;
const category = rawCategory ? cleanSlug(rawCategory) : undefined; const category = rawCategory ? cleanSlug(rawCategory) : undefined;
const tags = rawTags.map((tag: string) => cleanSlug(tag)); const tags = rawTags.map((tag: string) => cleanSlug(tag));
return { return {
id: id, id: id,
slug: slug, slug: slug,
permalink: await generatePermalink({ id, slug, publishDate, category }),
publishDate: publishDate, publishDate: publishDate,
updateDate: updateDate,
title: title,
excerpt: excerpt,
image: image,
category: category, category: category,
tags: tags, tags: tags,
author: author, author: author,
...rest, draft: draft,
metadata,
Content: Content, Content: Content,
// or 'body' in case you consume from API // or 'content' in case you consume from API
permalink: await generatePermalink({ id, slug, publishDate, category }),
readingTime: remarkPluginFrontmatter?.readingTime, readingTime: remarkPluginFrontmatter?.readingTime,
}; };
@ -78,6 +102,20 @@ const load = async function (): Promise<Array<Post>> {
let _posts: Array<Post>; let _posts: Array<Post>;
/** */
export const isBlogEnabled = APP_BLOG_CONFIG.isEnabled;
export const isBlogListRouteEnabled = APP_BLOG_CONFIG.list.isEnabled;
export const isBlogPostRouteEnabled = APP_BLOG_CONFIG.post.isEnabled;
export const isBlogCategoryRouteEnabled = APP_BLOG_CONFIG.category.isEnabled;
export const isBlogTagRouteEnabled = APP_BLOG_CONFIG.tag.isEnabled;
export const blogListRobots = APP_BLOG_CONFIG.list.robots;
export const blogPostRobots = APP_BLOG_CONFIG.post.robots;
export const blogCategoryRobots = APP_BLOG_CONFIG.category.robots;
export const blogTagRobots = APP_BLOG_CONFIG.tag.robots;
export const blogPostsPerPage = APP_BLOG_CONFIG?.postsPerPage;
/** */ /** */
export const fetchPosts = async (): Promise<Array<Post>> => { export const fetchPosts = async (): Promise<Array<Post>> => {
if (!_posts) { if (!_posts) {
@ -124,25 +162,71 @@ export const findLatestPosts = async ({ count }: { count?: number }): Promise<Ar
}; };
/** */ /** */
export const findTags = async (): Promise<Array<string>> => { export const getStaticPathsBlogList =
const posts = await fetchPosts(); () =>
const tags = posts.reduce((acc, post: Post) => { async ({ paginate }) => {
if (post.tags && Array.isArray(post.tags)) { if (!isBlogEnabled || !isBlogListRouteEnabled) return [];
return [...acc, ...post.tags]; return paginate(await fetchPosts(), {
} params: { blog: BLOG_BASE || undefined },
return acc; pageSize: blogPostsPerPage,
}, []); });
return [...new Set(tags)];
}; };
/** */ /** */
export const findCategories = async (): Promise<Array<string>> => { export const getStaticPathsBlogPost = () => async () => {
const posts = await fetchPosts(); if (!isBlogEnabled || !isBlogPostRouteEnabled) return [];
const categories = posts.reduce((acc, post: Post) => { return (await fetchPosts()).map((post) => ({
if (post.category) { params: {
return [...acc, post.category]; blog: post.permalink,
} },
return acc; props: { post },
}, []); }));
return [...new Set(categories)]; };
/** */
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());
});
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 [];
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 },
}
)
);
}; };

View File

@ -1,3 +1,7 @@
import { getImage } from '@astrojs/image';
import type { OpenGraph } from '@astrolib/seo/src/types';
import type { ImageMetadata } from 'astro';
const load = async function () { const load = async function () {
let images: Record<string, () => Promise<unknown>> | undefined = undefined; let images: Record<string, () => Promise<unknown>> | undefined = undefined;
try { try {
@ -8,12 +12,12 @@ const load = async function () {
return images; return images;
}; };
let _images; let _images: Record<string, () => Promise<unknown>> | undefined = undefined;
/** */ /** */
export const fetchLocalImages = async () => { export const fetchLocalImages = async () => {
_images = _images || load(); _images = _images || (await load());
return await _images; return _images;
}; };
/** */ /** */
@ -33,5 +37,58 @@ export const findImage = async (imagePath?: string) => {
const images = await fetchLocalImages(); const images = await fetchLocalImages();
const key = imagePath.replace('~/', '/src/'); const key = imagePath.replace('~/', '/src/');
return typeof images[key] === 'function' ? (await images[key]())['default'] : null; return images && typeof images[key] === 'function'
? ((await images[key]()) as { default: unknown })['default']
: null;
};
/** */
export const adaptOpenGraphImages = async (
openGraph: OpenGraph = {},
astroSite: URL | undefined = new URL('')
): Promise<OpenGraph> => {
if (!openGraph?.images?.length) {
return openGraph;
}
const images = openGraph.images;
const defaultWidth = 1200;
const defaultHeight = 626;
const adaptedImages = await Promise.all(
images.map(async (image) => {
if (image?.url) {
const resolvedImage = (await findImage(image.url)) as ImageMetadata | undefined;
if (!resolvedImage) {
return {
url: '',
};
}
const _image = await getImage({
src: resolvedImage,
alt: 'Placeholder alt',
width: image?.width || defaultWidth,
height: image?.height || defaultHeight,
});
if (typeof _image === 'object') {
return {
url: typeof _image.src === 'string' ? String(new URL(_image.src, astroSite)) : 'pepe',
width: typeof _image.width === 'number' ? _image.width : undefined,
height: typeof _image.height === 'number' ? _image.height : undefined,
};
}
return {
url: '',
};
}
return {
url: '',
};
})
);
return { ...openGraph, ...(adaptedImages ? { images: adaptedImages } : {}) };
}; };

View File

@ -1,6 +1,7 @@
import slugify from 'limax'; import slugify from 'limax';
import { SITE, BLOG } from '~/config.mjs'; import { SITE_CONFIG, APP_BLOG_CONFIG } from '~/utils/config';
import { trim } from '~/utils/utils'; import { trim } from '~/utils/utils';
export const trimSlash = (s: string) => trim(trim(s, '/')); export const trimSlash = (s: string) => trim(trim(s, '/'));
@ -9,10 +10,10 @@ const createPath = (...params: string[]) => {
.map((el) => trimSlash(el)) .map((el) => trimSlash(el))
.filter((el) => !!el) .filter((el) => !!el)
.join('/'); .join('/');
return '/' + paths + (SITE.trailingSlash && paths ? '/' : ''); return '/' + paths + (SITE_CONFIG.trailingSlash && paths ? '/' : '');
}; };
const BASE_PATHNAME = SITE.basePathname; const BASE_PATHNAME = SITE_CONFIG.base || '/';
export const cleanSlug = (text = '') => export const cleanSlug = (text = '') =>
trimSlash(text) trimSlash(text)
@ -20,18 +21,18 @@ export const cleanSlug = (text = '') =>
.map((slug) => slugify(slug)) .map((slug) => slugify(slug))
.join('/'); .join('/');
export const POST_PERMALINK_PATTERN = trimSlash(BLOG?.post?.permalink || '/%slug%'); export const BLOG_BASE = cleanSlug(APP_BLOG_CONFIG?.list?.pathname);
export const CATEGORY_BASE = cleanSlug(APP_BLOG_CONFIG?.category?.pathname);
export const TAG_BASE = cleanSlug(APP_BLOG_CONFIG?.tag?.pathname) || 'tag';
export const BLOG_BASE = cleanSlug(BLOG?.list?.pathname); export const POST_PERMALINK_PATTERN = trimSlash(APP_BLOG_CONFIG?.post?.permalink || `${BLOG_BASE}/%slug%`);
export const CATEGORY_BASE = cleanSlug(BLOG?.category?.pathname || 'category');
export const TAG_BASE = cleanSlug(BLOG?.tag?.pathname) || 'tag';
/** */ /** */
export const getCanonical = (path = ''): string | URL => { export const getCanonical = (path = ''): string | URL => {
const url = String(new URL(path, SITE.origin)); const url = String(new URL(path, SITE_CONFIG.site));
if (SITE.trailingSlash == false && path && url.endsWith('/')) { if (SITE_CONFIG.trailingSlash == false && path && url.endsWith('/')) {
return url.slice(0, -1); return url.slice(0, -1);
} else if (SITE.trailingSlash == true && path && !url.endsWith('/')) { } else if (SITE_CONFIG.trailingSlash == true && path && !url.endsWith('/')) {
return url + '/'; return url + '/';
} }
return url; return url;

View File

@ -1,7 +1,7 @@
import { DATE_FORMATTER } from '~/config.mjs'; import { I18N_CONFIG } from '~/utils/config';
const formatter = const formatter =
DATE_FORMATTER || I18N_CONFIG?.dateFormatter ||
new Intl.DateTimeFormat('en', { new Intl.DateTimeFormat('en', {
year: 'numeric', year: 'numeric',
month: 'short', month: 'short',
@ -10,7 +10,12 @@ const formatter =
}); });
/* eslint-disable no-mixed-spaces-and-tabs */ /* eslint-disable no-mixed-spaces-and-tabs */
export const getFormattedDate = (date: Date) => (date ? formatter.format(date) : ''); export const getFormattedDate = (date: Date) =>
date
? // eslint-disable-next-line @typescript-eslint/ban-ts-comment
/* @ts-ignore */
formatter.format(date)
: '';
export const trim = (str = '', ch?: string) => { export const trim = (str = '', ch?: string) => {
let start = 0, let start = 0,
@ -19,3 +24,37 @@ export const trim = (str = '', ch?: string) => {
while (end > start && str[end - 1] === ch) --end; while (end > start && str[end - 1] === ch) --end;
return start > 0 || end < str.length ? str.substring(start, end) : str; return start > 0 || end < str.length ? str.substring(start, end) : str;
}; };
// Function to format a number in thousands (K) or millions (M) format depending on its value
export const toUiAmount = (amount: number) => {
if (!amount) return 0;
let value;
if (amount >= 1000000000) {
const formattedNumber = (amount / 1000000000).toFixed(1);
if (Number(formattedNumber) === parseInt(formattedNumber)) {
value = parseInt(formattedNumber) + 'B';
} else {
value = formattedNumber + 'B';
}
} else if (amount >= 1000000) {
const formattedNumber = (amount / 1000000).toFixed(1);
if (Number(formattedNumber) === parseInt(formattedNumber)) {
value = parseInt(formattedNumber) + 'M';
} else {
value = formattedNumber + 'M';
}
} else if (amount >= 1000) {
const formattedNumber = (amount / 1000).toFixed(1);
if (Number(formattedNumber) === parseInt(formattedNumber)) {
value = parseInt(formattedNumber) + 'K';
} else {
value = formattedNumber + 'K';
}
} else {
value = Number(amount).toFixed(0);
}
return value;
};

View File

@ -8,7 +8,8 @@ module.exports = {
primary: 'var(--aw-color-primary)', primary: 'var(--aw-color-primary)',
secondary: 'var(--aw-color-secondary)', secondary: 'var(--aw-color-secondary)',
accent: 'var(--aw-color-accent)', accent: 'var(--aw-color-accent)',
muted: 'var(--aw-color-muted)', default: 'var(--aw-color-text-default)',
muted: 'var(--aw-color-text-muted)',
}, },
fontFamily: { fontFamily: {
sans: ['var(--aw-font-sans)', ...defaultTheme.fontFamily.sans], sans: ['var(--aw-font-sans)', ...defaultTheme.fontFamily.sans],