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
|
||||
│ │ └── ...
|
||||
│ ├── 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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 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' : ''}`}>
|
||||
|
@ -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} />
|
||||
)
|
||||
|
@ -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
|
||||
|
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) {
|
||||
if (theme === '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 { 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>
|
||||
|
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;
|
||||
---
|
||||
|
||||
<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) && (
|
||||
|
@ -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">
|
||||
{
|
||||
|
@ -44,7 +44,8 @@ const {
|
||||
aspectRatio="432:768"
|
||||
width={432}
|
||||
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';
|
||||
|
||||
const post = defineCollection({
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string().optional(),
|
||||
image: z.string().optional(),
|
||||
const metadataDefinition = () =>
|
||||
z
|
||||
.object({
|
||||
title: z.string().optional(),
|
||||
ignoreTitleTemplate: z.boolean().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(),
|
||||
|
||||
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,
|
||||
};
|
||||
|
@ -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,6 +8,7 @@ tags:
|
||||
- astro
|
||||
- tailwind css
|
||||
- front-end
|
||||
metadata:
|
||||
canonical: https://astrowind.vercel.app/astrowind-template-in-depth
|
||||
---
|
||||
|
||||
|
@ -1,13 +1,13 @@
|
||||
---
|
||||
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
|
||||
metadata:
|
||||
canonical: https://astrowind.vercel.app/get-started-website-with-astro-tailwind-css
|
||||
---
|
||||
|
||||
|
@ -7,6 +7,7 @@ tags:
|
||||
- astro
|
||||
- tailwind css
|
||||
- theme
|
||||
metadata:
|
||||
canonical: https://astrowind.vercel.app/how-to-customize-astrowind-to-your-brand
|
||||
---
|
||||
|
||||
|
@ -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
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 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 { 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
|
||||
|
@ -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>
|
||||
|
@ -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">
|
||||
|
@ -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"
|
||||
|
@ -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 = {
|
||||
|
||||
const metadata = {
|
||||
title: `Category '${category}' ${currentPage > 1 ? ` — Page ${currentPage}` : ''}`,
|
||||
description: SITE.description,
|
||||
noindex: BLOG?.category?.noindex,
|
||||
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} />
|
||||
|
@ -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} />
|
||||
|
@ -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 = {
|
||||
const url = getCanonical(getPermalink(post.permalink, 'post'));
|
||||
const image = (await findImage(post.image)) as ImageMetadata | undefined;
|
||||
|
||||
const metadata = merge(
|
||||
{
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
canonical: post.canonical || url,
|
||||
image: await findImage(post.image),
|
||||
noindex: BLOG?.post?.noindex,
|
||||
ogType: 'article',
|
||||
};
|
||||
description: post.excerpt,
|
||||
robots: {
|
||||
index: blogPostRobots?.index,
|
||||
follow: blogPostRobots?.follow,
|
||||
},
|
||||
openGraph: {
|
||||
type: 'article',
|
||||
...(image ? { images: [{ url: image?.src, width: image?.width, height: image?.height }] } : {}),
|
||||
},
|
||||
},
|
||||
{ ...(post?.metadata ? { ...post.metadata, canonical: post.metadata?.canonical || url } : {}) }
|
||||
);
|
||||
---
|
||||
|
||||
<Layout {meta}>
|
||||
<SinglePost post={{ ...post, image: meta.image }} url={url} />
|
||||
<Layout metadata={metadata}>
|
||||
<SinglePost post={{ ...post, image: image }} url={url} />
|
||||
<ToBlogLink />
|
||||
</Layout>
|
||||
|
@ -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>
|
||||
|
@ -1,11 +1,11 @@
|
||||
---
|
||||
import Layout from '~/layouts/PageLayout.astro';
|
||||
|
||||
const meta = {
|
||||
const metadata = {
|
||||
title: "Contact",
|
||||
};
|
||||
---
|
||||
|
||||
<Layout {meta}>
|
||||
<Layout metadata={metadata}>
|
||||
Contact
|
||||
</Layout>
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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"
|
||||
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
@ -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
81
src/types.d.ts
vendored
@ -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 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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -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 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 getStaticPathsBlogPost = () => async () => {
|
||||
if (!isBlogEnabled || !isBlogPostRouteEnabled) return [];
|
||||
return (await fetchPosts()).map((post) => ({
|
||||
params: {
|
||||
blog: post.permalink,
|
||||
},
|
||||
props: { post },
|
||||
}));
|
||||
};
|
||||
|
||||
/** */
|
||||
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 () {
|
||||
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 } : {}) };
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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],
|
||||
|
Reference in New Issue
Block a user