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 = () =>
z
.object({
title: z.string().optional(),
ignoreTitleTemplate: z.boolean().optional(),
canonical: z.string().url().optional(),
robots: z
.object({
index: z.boolean().optional(),
follow: z.boolean().optional(),
})
.optional(),
description: z.string().optional(),
openGraph: z
.object({
url: z.string().optional(),
siteName: z.string().optional(),
images: z
.array(
z.object({
url: z.string(),
width: z.number().optional(),
height: z.number().optional(),
})
)
.optional(),
locale: z.string().optional(),
type: z.string().optional(),
})
.optional(),
twitter: z
.object({
handle: z.string().optional(),
site: z.string().optional(),
cardType: z.string().optional(),
})
.optional(),
})
.optional();
const postCollection = defineCollection({
schema: z.object({ schema: z.object({
title: z.string(), publishDate: z.date().optional(),
description: z.string().optional(), updateDate: z.date().optional(),
image: z.string().optional(),
canonical: z.string().url().optional(),
publishDate: z.date().or(z.string()).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,7 +8,8 @@ tags:
- astro - astro
- tailwind css - tailwind css
- front-end - front-end
canonical: https://astrowind.vercel.app/astrowind-template-in-depth metadata:
canonical: https://astrowind.vercel.app/astrowind-template-in-depth
--- ---
import DListItem from '~/components/ui/DListItem.astro'; import DListItem from '~/components/ui/DListItem.astro';

View File

@ -1,14 +1,14 @@
--- ---
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
canonical: https://astrowind.vercel.app/get-started-website-with-astro-tailwind-css metadata:
canonical: https://astrowind.vercel.app/get-started-website-with-astro-tailwind-css
--- ---
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

View File

@ -7,7 +7,8 @@ tags:
- astro - astro
- tailwind css - tailwind css
- theme - theme
canonical: https://astrowind.vercel.app/how-to-customize-astrowind-to-your-brand metadata:
canonical: https://astrowind.vercel.app/how-to-customize-astrowind-to-your-brand
--- ---
## Congue justo vulputate nascetur convallis varius orci fringilla nulla pharetr ## Congue justo vulputate nascetur convallis varius orci fringilla nulla pharetr

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 = {
title: `Category'${category}' ${currentPage > 1 ? ` — Page ${currentPage}` : ''}`, const metadata = {
description: SITE.description, title: `Category '${category}' ${currentPage > 1 ? ` — Page ${currentPage}` : ''}`,
noindex: BLOG?.category?.noindex, robots: {
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'));
title: post.title, const image = (await findImage(post.image)) as ImageMetadata | undefined;
description: post.description,
canonical: post.canonical || url, const metadata = merge(
image: await findImage(post.image), {
noindex: BLOG?.post?.noindex, title: post.title,
ogType: 'article', description: post.excerpt,
}; robots: {
index: blogPostRobots?.index,
follow: blogPostRobots?.follow,
},
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 getStaticPathsBlogPost = () => async () => {
if (!isBlogEnabled || !isBlogPostRouteEnabled) return [];
return (await fetchPosts()).map((post) => ({
params: {
blog: post.permalink,
},
props: { post },
}));
}; };
/** */ /** */
export const findCategories = async (): Promise<Array<string>> => { export const getStaticPathsBlogCategory =
const posts = await fetchPosts(); () =>
const categories = posts.reduce((acc, post: Post) => { async ({ paginate }) => {
if (post.category) { if (!isBlogEnabled || !isBlogCategoryRouteEnabled) return [];
return [...acc, post.category];
} const posts = await fetchPosts();
return acc; const categories = new Set();
}, []); posts.map((post) => {
return [...new Set(categories)]; 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],