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
│ │ └── ...
│ ├── utils/
│ ├── config.mjs
│ ├── config.yaml
│ └── navigation.js
├── package.json
├── astro.config.mjs
@ -145,65 +145,79 @@ All commands are run from the root of the project, from a terminal:
### Configuration
Basic configuration file: `./src/config.mjs`
Basic configuration file: `./src/config.yaml`
```javascript
const CONFIG = {
name: 'Example',
```yaml
site:
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',
basePathname: '/', // Change this if you need to deploy to Github Pages, for example
trailingSlash: false, // Generate permalinks with or without "/" at the end
googleSiteVerificationId: orcPxI47GSa-cRvY11tUe6iGg2IO_RPvnA1q95iEM3M
title: 'Example - This is the homepage title of Example', // Default seo title
description: 'This is the homepage description of Example', // Default seo description
defaultImage: 'image.jpg', // Default seo image
# Default SEO metadata
metadata:
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
textDirection: 'ltr', // Default html text direction
apps:
blog:
isEnabled: true
postsPerPage: 6
dateFormatter: new Intl.DateTimeFormat('en', {
// Date format
year: 'numeric',
month: 'short',
day: 'numeric',
timeZone: 'UTC',
}),
post:
isEnabled: true
permalink: '/%slug%' # Variables: %slug%, %year%, %month%, %day%, %hour%, %minute%, %second%, %category%
robots:
index: true
googleAnalyticsId: false, // Or "G-XXXXXXXXXX",
googleSiteVerificationId: false, // Or some value,
list:
isEnabled: true
pathname: 'blog' # Blog main path, you can change this to "articles" (/articles)
robots:
index: true
blog: {
disabled: false,
postsPerPage: 4,
category:
isEnabled: true
pathname: 'category' # Category main path /category/some-category, you can change this to "group" (/group/some-category)
robots:
index: true
post: {
permalink: '/%slug%', // variables: %slug%, %year%, %month%, %day%, %hour%, %minute%, %second%, %category%
noindex: false,
disabled: false,
},
tag:
isEnabled: true
pathname: 'tag' # Tag main path /tag/some-tag, you can change this to "topics" (/topics/some-category)
robots:
index: false
list: {
pathname: 'blog', // Blog main path, you can change this to "articles" (/articles)
noindex: false,
disabled: false,
},
analytics:
vendors:
googleAnalytics:
isEnabled: false
id: null # or "G-XXXXXXXXXX"
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,
},
},
};
ui:
theme: 'system' # Values: "system" | "light" | "dark" | "light:only" | "dark:only"
```
<br>

View File

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

View File

@ -28,8 +28,27 @@ import '@fontsource-variable/inter';
--aw-color-primary: rgb(30 64 175);
--aw-color-secondary: rgb(30 58 138);
--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-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>

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">
🚀 {SITE?.name}
🚀 {SITE_CONFIG?.name}
</span>

View File

@ -1,7 +1,8 @@
---
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 { findImage } from '~/utils/images';
@ -12,7 +13,7 @@ export interface 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">
@ -38,7 +39,7 @@ const image = await findImage(post.image);
</div>
<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
) : (
<a
@ -50,5 +51,5 @@ const image = await findImage(post.image);
)
}
</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>

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 type { ImageMetadata } from 'astro';
import { Icon } from 'astro-icon/components';
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 { getPermalink } from '~/utils/permalinks';
@ -15,9 +16,9 @@ export interface 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' : ''}`}>

View File

@ -16,6 +16,7 @@ export interface Props {
}
const { post, url } = Astro.props;
const Content = post?.Content || null;
---
<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"
widths={[400, 900]}
sizes="(max-width: 900px) 400px, 900px"
alt={post.description || ''}
alt={post?.excerpt || ''}
loading="eager"
aspectRatio={16 / 9}
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"
>
{
post.Content ? (
<>
{/* @ts-ignore */}
<post.Content />
</>
Content ? (
<Content />
) : (
<Fragment set:html={post.content} />
)

View File

@ -1,7 +1,7 @@
---
import { getPermalink } from '~/utils/permalinks';
import { BLOG } from '~/config.mjs';
import { APP_BLOG_CONFIG } from '~/utils/config';
import type { Post } from '~/types';
export interface Props {
@ -23,7 +23,7 @@ const { tags, class: className = 'text-sm', title = undefined, isCategory = fals
<ul class={className}>
{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">
{BLOG?.tag?.disabled ? (
{!APP_BLOG_CONFIG?.tag?.isEnabled ? (
tag
) : (
<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) {
if (theme === '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 { SITE } from '~/config.mjs';
import { UI_CONFIG } from '~/utils/config';
export interface Props {
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>
<Icon name={iconName} class={iconClass} />
</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;
---
<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">
{
(title || subtitle || tagline) && (

View File

@ -1,6 +1,6 @@
---
import { Icon } from 'astro-icon/components';
import { SITE } from '~/config.mjs';
import { SITE_CONFIG } from '~/utils/config';
import { getHomePermalink } from '~/utils/permalinks';
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="col-span-12 lg:col-span-4">
<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 class="text-sm text-muted">
{

View File

@ -44,7 +44,8 @@ const {
aspectRatio="432:768"
width={432}
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';
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({
title: z.string(),
description: z.string().optional(),
image: z.string().optional(),
canonical: z.string().url().optional(),
publishDate: z.date().or(z.string()).optional(),
publishDate: z.date().optional(),
updateDate: z.date().optional(),
draft: z.boolean().optional(),
title: z.string(),
excerpt: z.string().optional(),
image: z.string().optional(),
category: z.string().optional(),
tags: z.array(z.string()).optional(),
author: z.string().optional(),
metadata: metadataDefinition(),
}),
});
export const collections = {
post: post,
post: postCollection,
};

View File

@ -1,7 +1,6 @@
---
publishDate: 2023-07-17T00:00:00Z
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.
image: ~/assets/images/stickers.jpg
category: Documentation
@ -9,7 +8,8 @@ tags:
- astro
- tailwind css
- 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';

View File

@ -1,14 +1,14 @@
---
publishDate: 2023-01-12T00:00:00Z
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.
image: ~/assets/images/do-more.jpg
category: Tutorials
tags:
- astro
- 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.

View File

@ -7,7 +7,8 @@ tags:
- astro
- tailwind css
- 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

View File

@ -1,7 +1,6 @@
---
publishDate: 2023-01-02T00:00:00Z
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.
tags:
- 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 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 { MetaSEO } from '~/types';
import { MetaData } from '~/types';
export interface Props {
frontmatter: {
@ -11,12 +11,12 @@ export interface Props {
const { frontmatter } = Astro.props;
const meta: MetaSEO = {
const metadata: MetaData = {
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">
<h1 class="font-bold font-heading text-4xl md:text-5xl leading-tighter tracking-tighter">{frontmatter.title}</h1>
<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 Footer from '~/components/widgets/Footer.astro';
import Announcement from '~/components/widgets/Announcement.astro';
import { headerData, footerData } from '~/navigation';
import { MetaSEO } from '~/types';
import { MetaData } from '~/types';
export interface Props {
meta?: MetaSEO;
metadata?: MetaData;
}
const { meta } = Astro.props;
const { metadata } = Astro.props;
---
<Layout {meta}>
<Layout metadata={metadata}>
<slot name="announcement">
<Announcement />
</slot>

View File

@ -1,11 +1,11 @@
---
import Layout from '~/layouts/BaseLayout.astro';
import Layout from '~/layouts/Layout.astro';
import { getHomePermalink } from '~/utils/permalinks';
const title = `Error 404`;
---
<Layout meta={{ title }}>
<Layout metadata={{ title }}>
<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="max-w-md text-center">

View File

@ -1,23 +1,13 @@
---
import { SITE, BLOG } from '~/config.mjs';
import Layout from '~/layouts/PageLayout.astro';
import BlogList from '~/components/blog/List.astro';
import Headline from '~/components/blog/Headline.astro';
import Pagination from '~/components/blog/Pagination.astro';
// import PostTags from "~/components/blog/Tags.astro";
import { fetchPosts } from '~/utils/blog';
// import { findTags, findCategories } from '~/utils/blog';
import { BLOG_BASE } from '~/utils/permalinks';
import { blogListRobots, getStaticPathsBlogList } from '~/utils/blog';
export async function getStaticPaths({ paginate }) {
if (BLOG?.disabled || BLOG?.list?.disabled) return [];
return paginate(await fetchPosts(), {
params: { blog: BLOG_BASE || undefined },
pageSize: BLOG.postsPerPage,
});
}
export const getStaticPaths = getStaticPathsBlogList();
const { page } = Astro.props;
const currentPage = page.currentPage ?? 1;
@ -25,15 +15,19 @@ const currentPage = page.currentPage ?? 1;
// const allCategories = await findCategories();
// const allTags = await findTags();
const meta = {
const metadata = {
title: `Blog${currentPage > 1 ? ` — Page ${currentPage}` : ''}`,
description: SITE.description,
noindex: BLOG?.list?.noindex || currentPage > 1,
ogType: 'blog',
robots: {
index: blogListRobots?.index && currentPage === 1,
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">
<Headline
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 BlogList from '~/components/blog/List.astro';
import Headline from '~/components/blog/Headline.astro';
import Pagination from '~/components/blog/Pagination.astro';
import { fetchPosts } from '~/utils/blog';
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 },
}
)
);
}
export const getStaticPaths = getStaticPathsBlogCategory();
const { page, category } = Astro.props;
const currentPage = page.currentPage ?? 1;
const meta = {
title: `Category'${category}' ${currentPage > 1 ? ` — Page ${currentPage}` : ''}`,
description: SITE.description,
noindex: BLOG?.category?.noindex,
const metadata = {
title: `Category '${category}' ${currentPage > 1 ? ` — Page ${currentPage}` : ''}`,
robots: {
index: blogCategoryRobots?.index,
follow: blogCategoryRobots?.follow,
},
};
---
<Layout meta={meta}>
<section class="px-6 sm:px-6 py-12 sm:py-16 lg:py-20 mx-auto max-w-4xl">
<Layout metadata={metadata}>
<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>
<BlogList posts={page.data} />
<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 BlogList from '~/components/blog/List.astro';
import Headline from '~/components/blog/Headline.astro';
import Pagination from '~/components/blog/Pagination.astro';
import { fetchPosts } from '~/utils/blog';
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 },
}
)
);
}
export const getStaticPaths = getStaticPathsBlogTag();
const { page, tag } = Astro.props;
const currentPage = page.currentPage ?? 1;
const meta = {
const metadata = {
title: `Posts by tag '${tag}'${currentPage > 1 ? ` — Page ${currentPage} ` : ''}`,
description: SITE.description,
noindex: BLOG?.tag?.noindex,
robots: {
index: blogTagRobots?.index,
follow: blogTagRobots?.follow,
},
};
---
<Layout meta={meta}>
<section class="px-6 sm:px-6 py-12 sm:py-16 lg:py-20 mx-auto max-w-4xl">
<Layout metadata={metadata}>
<section class="px-4 md:px-6 py-12 sm:py-16 lg:py-20 mx-auto max-w-4xl">
<Headline>Tag: {tag}</Headline>
<BlogList posts={page.data} />
<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 SinglePost from '~/components/blog/SinglePost.astro';
import ToBlogLink from '~/components/blog/ToBlogLink.astro';
import { getCanonical, getPermalink } from '~/utils/permalinks';
import { fetchPosts } from '~/utils/blog';
import { getStaticPathsBlogPost, blogPostRobots } from '~/utils/blog';
import { findImage } from '~/utils/images';
export async function getStaticPaths() {
if (BLOG?.disabled || BLOG?.post?.disabled) return [];
return (await fetchPosts()).map((post) => ({
params: {
blog: post.permalink,
},
props: { post },
}));
}
export const getStaticPaths = getStaticPathsBlogPost();
const { post } = Astro.props;
const url = getCanonical(getPermalink(post.permalink, 'post'));
const meta = {
title: post.title,
description: post.description,
canonical: post.canonical || url,
image: await findImage(post.image),
noindex: BLOG?.post?.noindex,
ogType: 'article',
};
const url = getCanonical(getPermalink(post.permalink, 'post'));
const image = (await findImage(post.image)) as ImageMetadata | undefined;
const metadata = merge(
{
title: post.title,
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}>
<SinglePost post={{ ...post, image: meta.image }} url={url} />
<Layout metadata={metadata}>
<SinglePost post={{ ...post, image: image }} url={url} />
<ToBlogLink />
</Layout>

View File

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

View File

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

View File

@ -1,5 +1,4 @@
---
import { SITE } from '~/config.mjs';
import Layout from '~/layouts/PageLayout.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 Steps from '~/components/widgets/Steps.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 Stats from '~/components/widgets/Stats.astro';
import CallToAction from '~/components/widgets/CallToAction.astro';
const meta = {
title: SITE.title,
description: SITE.description,
const metadata = {
title: "AstroWind — Free template for create a website with Astro + Tailwind CSS",
dontUseTitleTemplate: true,
};
---
<Layout {meta}>
<Layout metadata={metadata}>
<!-- Hero Widget ******************* -->
<Hero
@ -262,7 +260,7 @@ const meta = {
<!-- HighlightedPosts Widget ******* -->
<LatestPosts
<BlogLatestPosts
title="Find out more content in our Blog"
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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

81
src/types.d.ts vendored
View File

@ -1,42 +1,83 @@
import { AstroComponentFactory } from 'astro/dist/runtime/server';
export interface Post {
/** A unique ID number that identifies a post. */
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;
publishDate: Date;
title: string;
description?: string;
/** */
permalink: string;
/** */
publishDate: Date;
/** */
updateDate?: Date;
/** */
title: string;
/** Optional summary of post content. */
excerpt?: string;
/** */
image?: string;
canonical?: string | URL;
permalink?: string;
draft?: boolean;
excerpt?: string;
/** */
category?: string;
/** */
tags?: Array<string>;
/** */
author?: string;
Content: AstroComponentFactory;
/** */
metadata?: MetaData;
/** */
draft?: boolean;
/** */
Content?: unknown;
content?: string;
/** */
readingTime?: number;
}
export interface MetaSEO {
export interface MetaData {
title?: string;
ignoreTitleTemplate?: boolean;
canonical?: string;
robots?: MetaDataRobots;
description?: string;
image?: string;
canonical?: string | URL;
noindex?: boolean;
nofollow?: boolean;
openGraph?: MetaDataOpenGraph;
twitter?: MetaDataTwitter;
}
ogTitle?: string;
ogType?: string;
export interface MetaDataRobots {
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 {
@ -189,7 +230,7 @@ export interface Steps extends Headline, Widget {
icon?: string;
classes?: Record<string, string>;
}>;
image?: string | any; // TODO: find HTMLElementProps
image?: string | Image;
isReversed?: boolean;
}

View File

@ -1,9 +1,20 @@
import { getCollection } from 'astro:content';
import type { CollectionEntry } from 'astro:content';
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 month = String(publishDate.getMonth() + 1).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 {
publishDate: rawPublishDate = new Date(),
updateDate: rawUpdateDate,
title,
excerpt,
image,
tags: rawTags = [],
category: rawCategory,
author = 'Anonymous',
publishDate: rawPublishDate = new Date(),
...rest
author,
draft = false,
metadata = {},
} = data;
const slug = cleanSlug(rawSlug.split('/').pop());
const publishDate = new Date(rawPublishDate);
const updateDate = rawUpdateDate ? new Date(rawUpdateDate) : undefined;
const category = rawCategory ? cleanSlug(rawCategory) : undefined;
const tags = rawTags.map((tag: string) => cleanSlug(tag));
return {
id: id,
slug: slug,
permalink: await generatePermalink({ id, slug, publishDate, category }),
publishDate: publishDate,
updateDate: updateDate,
title: title,
excerpt: excerpt,
image: image,
category: category,
tags: tags,
author: author,
...rest,
draft: draft,
metadata,
Content: Content,
// or 'body' in case you consume from API
permalink: await generatePermalink({ id, slug, publishDate, category }),
// or 'content' in case you consume from API
readingTime: remarkPluginFrontmatter?.readingTime,
};
@ -78,6 +102,20 @@ const load = async function (): Promise<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>> => {
if (!_posts) {
@ -124,25 +162,71 @@ export const findLatestPosts = async ({ count }: { count?: number }): Promise<Ar
};
/** */
export const findTags = async (): Promise<Array<string>> => {
const posts = await fetchPosts();
const tags = posts.reduce((acc, post: Post) => {
if (post.tags && Array.isArray(post.tags)) {
return [...acc, ...post.tags];
}
return acc;
}, []);
return [...new Set(tags)];
export const getStaticPathsBlogList =
() =>
async ({ paginate }) => {
if (!isBlogEnabled || !isBlogListRouteEnabled) return [];
return paginate(await fetchPosts(), {
params: { blog: BLOG_BASE || undefined },
pageSize: blogPostsPerPage,
});
};
/** */
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>> => {
const posts = await fetchPosts();
const categories = posts.reduce((acc, post: Post) => {
if (post.category) {
return [...acc, post.category];
}
return acc;
}, []);
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 () {
let images: Record<string, () => Promise<unknown>> | undefined = undefined;
try {
@ -8,12 +12,12 @@ const load = async function () {
return images;
};
let _images;
let _images: Record<string, () => Promise<unknown>> | undefined = undefined;
/** */
export const fetchLocalImages = async () => {
_images = _images || load();
return await _images;
_images = _images || (await load());
return _images;
};
/** */
@ -33,5 +37,58 @@ export const findImage = async (imagePath?: string) => {
const images = await fetchLocalImages();
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 { SITE, BLOG } from '~/config.mjs';
import { SITE_CONFIG, APP_BLOG_CONFIG } from '~/utils/config';
import { trim } from '~/utils/utils';
export const trimSlash = (s: string) => trim(trim(s, '/'));
@ -9,10 +10,10 @@ const createPath = (...params: string[]) => {
.map((el) => trimSlash(el))
.filter((el) => !!el)
.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 = '') =>
trimSlash(text)
@ -20,18 +21,18 @@ export const cleanSlug = (text = '') =>
.map((slug) => slugify(slug))
.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 CATEGORY_BASE = cleanSlug(BLOG?.category?.pathname || 'category');
export const TAG_BASE = cleanSlug(BLOG?.tag?.pathname) || 'tag';
export const POST_PERMALINK_PATTERN = trimSlash(APP_BLOG_CONFIG?.post?.permalink || `${BLOG_BASE}/%slug%`);
/** */
export const getCanonical = (path = ''): string | URL => {
const url = String(new URL(path, SITE.origin));
if (SITE.trailingSlash == false && path && url.endsWith('/')) {
const url = String(new URL(path, SITE_CONFIG.site));
if (SITE_CONFIG.trailingSlash == false && path && url.endsWith('/')) {
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;

View File

@ -1,7 +1,7 @@
import { DATE_FORMATTER } from '~/config.mjs';
import { I18N_CONFIG } from '~/utils/config';
const formatter =
DATE_FORMATTER ||
I18N_CONFIG?.dateFormatter ||
new Intl.DateTimeFormat('en', {
year: 'numeric',
month: 'short',
@ -10,7 +10,12 @@ const formatter =
});
/* 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) => {
let start = 0,
@ -19,3 +24,37 @@ export const trim = (str = '', ch?: string) => {
while (end > start && str[end - 1] === ch) --end;
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)',
secondary: 'var(--aw-color-secondary)',
accent: 'var(--aw-color-accent)',
muted: 'var(--aw-color-muted)',
default: 'var(--aw-color-text-default)',
muted: 'var(--aw-color-text-muted)',
},
fontFamily: {
sans: ['var(--aw-font-sans)', ...defaultTheme.fontFamily.sans],