Add support for new config.yaml
This commit is contained in:
112
README.md
112
README.md
@ -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>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
|
@ -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>
|
|
@ -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' : ''}`}>
|
||||||
|
@ -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} />
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
8
src/components/common/Analytics.astro
Normal file
8
src/components/common/Analytics.astro
Normal 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} />}
|
33
src/components/common/ApplyColorMode.astro
Normal file
33
src/components/common/ApplyColorMode.astro
Normal 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>
|
@ -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');
|
||||||
|
8
src/components/common/CommonMeta.astro
Normal file
8
src/components/common/CommonMeta.astro
Normal 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')} />
|
@ -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')} />
|
|
68
src/components/common/Metadata.astro
Normal file
68
src/components/common/Metadata.astro
Normal 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) }} />
|
5
src/components/common/SiteVerification.astro
Normal file
5
src/components/common/SiteVerification.astro
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
import { SITE_CONFIG } from "~/utils/config";
|
||||||
|
---
|
||||||
|
|
||||||
|
{SITE_CONFIG.googleSiteVerificationId && <meta name="google-site-verification" content={SITE_CONFIG.googleSiteVerificationId} />}
|
@ -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>
|
||||||
|
64
src/components/widgets/BlogHighlightedPosts.astro
Normal file
64
src/components/widgets/BlogHighlightedPosts.astro
Normal 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 />
|
||||||
|
)
|
||||||
|
}
|
64
src/components/widgets/BlogLatestPosts.astro
Normal file
64
src/components/widgets/BlogLatestPosts.astro
Normal 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 />
|
||||||
|
)
|
||||||
|
}
|
@ -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) && (
|
||||||
|
@ -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">
|
||||||
{
|
{
|
||||||
|
@ -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 || ""}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
@ -1,23 +1,68 @@
|
|||||||
import { z, defineCollection } from 'astro:content';
|
import { z, defineCollection } from 'astro:content';
|
||||||
|
|
||||||
const post = defineCollection({
|
const metadataDefinition = () =>
|
||||||
schema: z.object({
|
z
|
||||||
title: z.string(),
|
.object({
|
||||||
description: z.string().optional(),
|
title: z.string().optional(),
|
||||||
image: z.string().optional(),
|
ignoreTitleTemplate: z.boolean().optional(),
|
||||||
|
|
||||||
canonical: z.string().url().optional(),
|
canonical: z.string().url().optional(),
|
||||||
|
|
||||||
publishDate: z.date().or(z.string()).optional(),
|
robots: z
|
||||||
|
.object({
|
||||||
|
index: z.boolean().optional(),
|
||||||
|
follow: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
|
||||||
|
description: z.string().optional(),
|
||||||
|
|
||||||
|
openGraph: z
|
||||||
|
.object({
|
||||||
|
url: z.string().optional(),
|
||||||
|
siteName: z.string().optional(),
|
||||||
|
images: z
|
||||||
|
.array(
|
||||||
|
z.object({
|
||||||
|
url: z.string(),
|
||||||
|
width: z.number().optional(),
|
||||||
|
height: z.number().optional(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
locale: z.string().optional(),
|
||||||
|
type: z.string().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
|
||||||
|
twitter: z
|
||||||
|
.object({
|
||||||
|
handle: z.string().optional(),
|
||||||
|
site: z.string().optional(),
|
||||||
|
cardType: z.string().optional(),
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
})
|
||||||
|
.optional();
|
||||||
|
|
||||||
|
const postCollection = defineCollection({
|
||||||
|
schema: z.object({
|
||||||
|
publishDate: z.date().optional(),
|
||||||
|
updateDate: z.date().optional(),
|
||||||
draft: z.boolean().optional(),
|
draft: z.boolean().optional(),
|
||||||
|
|
||||||
|
title: z.string(),
|
||||||
excerpt: z.string().optional(),
|
excerpt: z.string().optional(),
|
||||||
|
image: z.string().optional(),
|
||||||
|
|
||||||
category: z.string().optional(),
|
category: z.string().optional(),
|
||||||
tags: z.array(z.string()).optional(),
|
tags: z.array(z.string()).optional(),
|
||||||
author: z.string().optional(),
|
author: z.string().optional(),
|
||||||
|
|
||||||
|
metadata: metadataDefinition(),
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const collections = {
|
export const collections = {
|
||||||
post: post,
|
post: postCollection,
|
||||||
};
|
};
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
publishDate: 2023-07-17T00:00:00Z
|
publishDate: 2023-07-17T00:00:00Z
|
||||||
title: AstroWind template in depth
|
title: AstroWind template in depth
|
||||||
description: Internals documentation
|
|
||||||
excerpt: While easy to get started, Astrowind is quite complex internally. This page provides documentation on some of the more intricate parts.
|
excerpt: While easy to get started, Astrowind is quite complex internally. This page provides documentation on some of the more intricate parts.
|
||||||
image: ~/assets/images/stickers.jpg
|
image: ~/assets/images/stickers.jpg
|
||||||
category: Documentation
|
category: Documentation
|
||||||
@ -9,6 +8,7 @@ tags:
|
|||||||
- astro
|
- astro
|
||||||
- tailwind css
|
- tailwind css
|
||||||
- front-end
|
- front-end
|
||||||
|
metadata:
|
||||||
canonical: https://astrowind.vercel.app/astrowind-template-in-depth
|
canonical: https://astrowind.vercel.app/astrowind-template-in-depth
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
---
|
---
|
||||||
publishDate: 2023-01-12T00:00:00Z
|
publishDate: 2023-01-12T00:00:00Z
|
||||||
title: Get started with AstroWind to create a website using Astro and Tailwind CSS
|
title: Get started with AstroWind to create a website using Astro and Tailwind CSS
|
||||||
description: Lorem ipsum dolor sit amet
|
|
||||||
excerpt: Sint sit cillum pariatur eiusmod nulla pariatur ipsum. Sit laborum anim qui mollit tempor pariatur.
|
excerpt: Sint sit cillum pariatur eiusmod nulla pariatur ipsum. Sit laborum anim qui mollit tempor pariatur.
|
||||||
image: ~/assets/images/do-more.jpg
|
image: ~/assets/images/do-more.jpg
|
||||||
category: Tutorials
|
category: Tutorials
|
||||||
tags:
|
tags:
|
||||||
- astro
|
- astro
|
||||||
- tailwind css
|
- tailwind css
|
||||||
|
metadata:
|
||||||
canonical: https://astrowind.vercel.app/get-started-website-with-astro-tailwind-css
|
canonical: https://astrowind.vercel.app/get-started-website-with-astro-tailwind-css
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ tags:
|
|||||||
- astro
|
- astro
|
||||||
- tailwind css
|
- tailwind css
|
||||||
- theme
|
- theme
|
||||||
|
metadata:
|
||||||
canonical: https://astrowind.vercel.app/how-to-customize-astrowind-to-your-brand
|
canonical: https://astrowind.vercel.app/how-to-customize-astrowind-to-your-brand
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -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
1
src/env.d.ts
vendored
@ -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" />
|
||||||
|
@ -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
49
src/layouts/Layout.astro
Normal 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>
|
@ -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
|
||||||
|
@ -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>
|
||||||
|
@ -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">
|
||||||
|
@ -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"
|
||||||
|
@ -1,47 +1,28 @@
|
|||||||
---
|
---
|
||||||
import { SITE, BLOG } from '~/config.mjs';
|
import { blogCategoryRobots, getStaticPathsBlogCategory } from '~/utils/blog';
|
||||||
|
|
||||||
import Layout from '~/layouts/PageLayout.astro';
|
import Layout from '~/layouts/PageLayout.astro';
|
||||||
import BlogList from '~/components/blog/List.astro';
|
import BlogList from '~/components/blog/List.astro';
|
||||||
import Headline from '~/components/blog/Headline.astro';
|
import Headline from '~/components/blog/Headline.astro';
|
||||||
import Pagination from '~/components/blog/Pagination.astro';
|
import Pagination from '~/components/blog/Pagination.astro';
|
||||||
|
|
||||||
import { fetchPosts } from '~/utils/blog';
|
export const getStaticPaths = getStaticPathsBlogCategory();
|
||||||
import { CATEGORY_BASE } from '~/utils/permalinks';
|
|
||||||
|
|
||||||
export async function getStaticPaths({ paginate }) {
|
|
||||||
if (BLOG?.disabled || BLOG?.category?.disabled) return [];
|
|
||||||
|
|
||||||
const posts = await fetchPosts();
|
|
||||||
const categories = new Set();
|
|
||||||
posts.map((post) => {
|
|
||||||
typeof post.category === 'string' && categories.add(post.category.toLowerCase());
|
|
||||||
});
|
|
||||||
|
|
||||||
return Array.from(categories).map((category: string) =>
|
|
||||||
paginate(
|
|
||||||
posts.filter((post) => typeof post.category === 'string' && category === post.category.toLowerCase()),
|
|
||||||
{
|
|
||||||
params: { category: category, blog: CATEGORY_BASE || undefined },
|
|
||||||
pageSize: BLOG.postsPerPage,
|
|
||||||
props: { category },
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { page, category } = Astro.props;
|
const { page, category } = Astro.props;
|
||||||
|
|
||||||
const currentPage = page.currentPage ?? 1;
|
const currentPage = page.currentPage ?? 1;
|
||||||
const meta = {
|
|
||||||
|
const metadata = {
|
||||||
title: `Category '${category}' ${currentPage > 1 ? ` — Page ${currentPage}` : ''}`,
|
title: `Category '${category}' ${currentPage > 1 ? ` — Page ${currentPage}` : ''}`,
|
||||||
description: SITE.description,
|
robots: {
|
||||||
noindex: BLOG?.category?.noindex,
|
index: blogCategoryRobots?.index,
|
||||||
|
follow: blogCategoryRobots?.follow,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout meta={meta}>
|
<Layout metadata={metadata}>
|
||||||
<section class="px-6 sm:px-6 py-12 sm:py-16 lg:py-20 mx-auto max-w-4xl">
|
<section class="px-4 md:px-6 py-12 sm:py-16 lg:py-20 mx-auto max-w-4xl">
|
||||||
<Headline><span class="capitalize">{category.replaceAll('-', ' ')}</span></Headline>
|
<Headline><span class="capitalize">{category.replaceAll('-', ' ')}</span></Headline>
|
||||||
<BlogList posts={page.data} />
|
<BlogList posts={page.data} />
|
||||||
<Pagination prevUrl={page.url.prev} nextUrl={page.url.next} />
|
<Pagination prevUrl={page.url.prev} nextUrl={page.url.next} />
|
||||||
|
@ -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} />
|
||||||
|
@ -1,38 +1,39 @@
|
|||||||
---
|
---
|
||||||
import { BLOG } from '~/config.mjs';
|
import merge from 'lodash.merge';
|
||||||
|
import type { ImageMetadata } from 'astro';
|
||||||
import Layout from '~/layouts/PageLayout.astro';
|
import Layout from '~/layouts/PageLayout.astro';
|
||||||
import SinglePost from '~/components/blog/SinglePost.astro';
|
import SinglePost from '~/components/blog/SinglePost.astro';
|
||||||
import ToBlogLink from '~/components/blog/ToBlogLink.astro';
|
import ToBlogLink from '~/components/blog/ToBlogLink.astro';
|
||||||
|
|
||||||
import { getCanonical, getPermalink } from '~/utils/permalinks';
|
import { getCanonical, getPermalink } from '~/utils/permalinks';
|
||||||
import { fetchPosts } from '~/utils/blog';
|
import { getStaticPathsBlogPost, blogPostRobots } from '~/utils/blog';
|
||||||
import { findImage } from '~/utils/images';
|
import { findImage } from '~/utils/images';
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export const getStaticPaths = getStaticPathsBlogPost();
|
||||||
if (BLOG?.disabled || BLOG?.post?.disabled) return [];
|
|
||||||
return (await fetchPosts()).map((post) => ({
|
|
||||||
params: {
|
|
||||||
blog: post.permalink,
|
|
||||||
},
|
|
||||||
props: { post },
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
const { post } = Astro.props;
|
const { post } = Astro.props;
|
||||||
const url = getCanonical(getPermalink(post.permalink, 'post'));
|
|
||||||
|
|
||||||
const meta = {
|
const url = getCanonical(getPermalink(post.permalink, 'post'));
|
||||||
|
const image = (await findImage(post.image)) as ImageMetadata | undefined;
|
||||||
|
|
||||||
|
const metadata = merge(
|
||||||
|
{
|
||||||
title: post.title,
|
title: post.title,
|
||||||
description: post.description,
|
description: post.excerpt,
|
||||||
canonical: post.canonical || url,
|
robots: {
|
||||||
image: await findImage(post.image),
|
index: blogPostRobots?.index,
|
||||||
noindex: BLOG?.post?.noindex,
|
follow: blogPostRobots?.follow,
|
||||||
ogType: 'article',
|
},
|
||||||
};
|
openGraph: {
|
||||||
|
type: 'article',
|
||||||
|
...(image ? { images: [{ url: image?.src, width: image?.width, height: image?.height }] } : {}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ ...(post?.metadata ? { ...post.metadata, canonical: post.metadata?.canonical || url } : {}) }
|
||||||
|
);
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout {meta}>
|
<Layout metadata={metadata}>
|
||||||
<SinglePost post={{ ...post, image: meta.image }} url={url} />
|
<SinglePost post={{ ...post, image: image }} url={url} />
|
||||||
<ToBlogLink />
|
<ToBlogLink />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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.
|
||||||
|
@ -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
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
|
@ -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"
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -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
81
src/types.d.ts
vendored
@ -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 post’s unique slug – part of the post’s 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,9 +1,20 @@
|
|||||||
import { getCollection } from 'astro:content';
|
import { getCollection } from 'astro:content';
|
||||||
import type { CollectionEntry } from 'astro:content';
|
import type { CollectionEntry } from 'astro:content';
|
||||||
import type { Post } from '~/types';
|
import type { Post } from '~/types';
|
||||||
import { cleanSlug, trimSlash, POST_PERMALINK_PATTERN } from './permalinks';
|
import { APP_BLOG_CONFIG } from '~/utils/config';
|
||||||
|
import { cleanSlug, trimSlash, BLOG_BASE, POST_PERMALINK_PATTERN, CATEGORY_BASE, TAG_BASE } from './permalinks';
|
||||||
|
|
||||||
const generatePermalink = async ({ id, slug, publishDate, category }) => {
|
const generatePermalink = async ({
|
||||||
|
id,
|
||||||
|
slug,
|
||||||
|
publishDate,
|
||||||
|
category,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
slug: string;
|
||||||
|
publishDate: Date;
|
||||||
|
category: string | undefined;
|
||||||
|
}) => {
|
||||||
const year = String(publishDate.getFullYear()).padStart(4, '0');
|
const year = String(publishDate.getFullYear()).padStart(4, '0');
|
||||||
const month = String(publishDate.getMonth() + 1).padStart(2, '0');
|
const month = String(publishDate.getMonth() + 1).padStart(2, '0');
|
||||||
const day = String(publishDate.getDate()).padStart(2, '0');
|
const day = String(publishDate.getDate()).padStart(2, '0');
|
||||||
@ -33,33 +44,46 @@ const getNormalizedPost = async (post: CollectionEntry<'post'>): Promise<Post> =
|
|||||||
const { Content, remarkPluginFrontmatter } = await post.render();
|
const { Content, remarkPluginFrontmatter } = await post.render();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
publishDate: rawPublishDate = new Date(),
|
||||||
|
updateDate: rawUpdateDate,
|
||||||
|
title,
|
||||||
|
excerpt,
|
||||||
|
image,
|
||||||
tags: rawTags = [],
|
tags: rawTags = [],
|
||||||
category: rawCategory,
|
category: rawCategory,
|
||||||
author = 'Anonymous',
|
author,
|
||||||
publishDate: rawPublishDate = new Date(),
|
draft = false,
|
||||||
...rest
|
metadata = {},
|
||||||
} = data;
|
} = data;
|
||||||
|
|
||||||
const slug = cleanSlug(rawSlug.split('/').pop());
|
const slug = cleanSlug(rawSlug.split('/').pop());
|
||||||
const publishDate = new Date(rawPublishDate);
|
const publishDate = new Date(rawPublishDate);
|
||||||
|
const updateDate = rawUpdateDate ? new Date(rawUpdateDate) : undefined;
|
||||||
const category = rawCategory ? cleanSlug(rawCategory) : undefined;
|
const category = rawCategory ? cleanSlug(rawCategory) : undefined;
|
||||||
const tags = rawTags.map((tag: string) => cleanSlug(tag));
|
const tags = rawTags.map((tag: string) => cleanSlug(tag));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: id,
|
id: id,
|
||||||
slug: slug,
|
slug: slug,
|
||||||
|
permalink: await generatePermalink({ id, slug, publishDate, category }),
|
||||||
|
|
||||||
publishDate: publishDate,
|
publishDate: publishDate,
|
||||||
|
updateDate: updateDate,
|
||||||
|
|
||||||
|
title: title,
|
||||||
|
excerpt: excerpt,
|
||||||
|
image: image,
|
||||||
|
|
||||||
category: category,
|
category: category,
|
||||||
tags: tags,
|
tags: tags,
|
||||||
author: author,
|
author: author,
|
||||||
|
|
||||||
...rest,
|
draft: draft,
|
||||||
|
|
||||||
|
metadata,
|
||||||
|
|
||||||
Content: Content,
|
Content: Content,
|
||||||
// or 'body' in case you consume from API
|
// or 'content' in case you consume from API
|
||||||
|
|
||||||
permalink: await generatePermalink({ id, slug, publishDate, category }),
|
|
||||||
|
|
||||||
readingTime: remarkPluginFrontmatter?.readingTime,
|
readingTime: remarkPluginFrontmatter?.readingTime,
|
||||||
};
|
};
|
||||||
@ -78,6 +102,20 @@ const load = async function (): Promise<Array<Post>> {
|
|||||||
|
|
||||||
let _posts: Array<Post>;
|
let _posts: Array<Post>;
|
||||||
|
|
||||||
|
/** */
|
||||||
|
export const isBlogEnabled = APP_BLOG_CONFIG.isEnabled;
|
||||||
|
export const isBlogListRouteEnabled = APP_BLOG_CONFIG.list.isEnabled;
|
||||||
|
export const isBlogPostRouteEnabled = APP_BLOG_CONFIG.post.isEnabled;
|
||||||
|
export const isBlogCategoryRouteEnabled = APP_BLOG_CONFIG.category.isEnabled;
|
||||||
|
export const isBlogTagRouteEnabled = APP_BLOG_CONFIG.tag.isEnabled;
|
||||||
|
|
||||||
|
export const blogListRobots = APP_BLOG_CONFIG.list.robots;
|
||||||
|
export const blogPostRobots = APP_BLOG_CONFIG.post.robots;
|
||||||
|
export const blogCategoryRobots = APP_BLOG_CONFIG.category.robots;
|
||||||
|
export const blogTagRobots = APP_BLOG_CONFIG.tag.robots;
|
||||||
|
|
||||||
|
export const blogPostsPerPage = APP_BLOG_CONFIG?.postsPerPage;
|
||||||
|
|
||||||
/** */
|
/** */
|
||||||
export const fetchPosts = async (): Promise<Array<Post>> => {
|
export const fetchPosts = async (): Promise<Array<Post>> => {
|
||||||
if (!_posts) {
|
if (!_posts) {
|
||||||
@ -124,25 +162,71 @@ export const findLatestPosts = async ({ count }: { count?: number }): Promise<Ar
|
|||||||
};
|
};
|
||||||
|
|
||||||
/** */
|
/** */
|
||||||
export const findTags = async (): Promise<Array<string>> => {
|
export const getStaticPathsBlogList =
|
||||||
const posts = await fetchPosts();
|
() =>
|
||||||
const tags = posts.reduce((acc, post: Post) => {
|
async ({ paginate }) => {
|
||||||
if (post.tags && Array.isArray(post.tags)) {
|
if (!isBlogEnabled || !isBlogListRouteEnabled) return [];
|
||||||
return [...acc, ...post.tags];
|
return paginate(await fetchPosts(), {
|
||||||
}
|
params: { blog: BLOG_BASE || undefined },
|
||||||
return acc;
|
pageSize: blogPostsPerPage,
|
||||||
}, []);
|
});
|
||||||
return [...new Set(tags)];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/** */
|
/** */
|
||||||
export const findCategories = async (): Promise<Array<string>> => {
|
export const getStaticPathsBlogPost = () => async () => {
|
||||||
const posts = await fetchPosts();
|
if (!isBlogEnabled || !isBlogPostRouteEnabled) return [];
|
||||||
const categories = posts.reduce((acc, post: Post) => {
|
return (await fetchPosts()).map((post) => ({
|
||||||
if (post.category) {
|
params: {
|
||||||
return [...acc, post.category];
|
blog: post.permalink,
|
||||||
}
|
},
|
||||||
return acc;
|
props: { post },
|
||||||
}, []);
|
}));
|
||||||
return [...new Set(categories)];
|
};
|
||||||
|
|
||||||
|
/** */
|
||||||
|
export const getStaticPathsBlogCategory =
|
||||||
|
() =>
|
||||||
|
async ({ paginate }) => {
|
||||||
|
if (!isBlogEnabled || !isBlogCategoryRouteEnabled) return [];
|
||||||
|
|
||||||
|
const posts = await fetchPosts();
|
||||||
|
const categories = new Set();
|
||||||
|
posts.map((post) => {
|
||||||
|
typeof post.category === 'string' && categories.add(post.category.toLowerCase());
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(categories).map((category: string) =>
|
||||||
|
paginate(
|
||||||
|
posts.filter((post) => typeof post.category === 'string' && category === post.category.toLowerCase()),
|
||||||
|
{
|
||||||
|
params: { category: category, blog: CATEGORY_BASE || undefined },
|
||||||
|
pageSize: blogPostsPerPage,
|
||||||
|
props: { category },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** */
|
||||||
|
export const getStaticPathsBlogTag =
|
||||||
|
() =>
|
||||||
|
async ({ paginate }) => {
|
||||||
|
if (!isBlogEnabled || !isBlogTagRouteEnabled) return [];
|
||||||
|
|
||||||
|
const posts = await fetchPosts();
|
||||||
|
const tags = new Set();
|
||||||
|
posts.map((post) => {
|
||||||
|
Array.isArray(post.tags) && post.tags.map((tag) => tags.add(tag.toLowerCase()));
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(tags).map((tag: string) =>
|
||||||
|
paginate(
|
||||||
|
posts.filter((post) => Array.isArray(post.tags) && post.tags.find((elem) => elem.toLowerCase() === tag)),
|
||||||
|
{
|
||||||
|
params: { tag: tag, blog: TAG_BASE || undefined },
|
||||||
|
pageSize: blogPostsPerPage,
|
||||||
|
props: { tag },
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
@ -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 } : {}) };
|
||||||
};
|
};
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
};
|
||||||
|
@ -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],
|
||||||
|
Reference in New Issue
Block a user