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({
|
||||
const metadataDefinition = () =>
|
||||
z
|
||||
.object({
|
||||
title: z.string().optional(),
|
||||
ignoreTitleTemplate: z.boolean().optional(),
|
||||
|
||||
canonical: z.string().url().optional(),
|
||||
|
||||
robots: z
|
||||
.object({
|
||||
index: z.boolean().optional(),
|
||||
follow: z.boolean().optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
description: z.string().optional(),
|
||||
|
||||
openGraph: z
|
||||
.object({
|
||||
url: z.string().optional(),
|
||||
siteName: z.string().optional(),
|
||||
images: z
|
||||
.array(
|
||||
z.object({
|
||||
url: z.string(),
|
||||
width: z.number().optional(),
|
||||
height: z.number().optional(),
|
||||
})
|
||||
)
|
||||
.optional(),
|
||||
locale: z.string().optional(),
|
||||
type: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
|
||||
twitter: z
|
||||
.object({
|
||||
handle: z.string().optional(),
|
||||
site: z.string().optional(),
|
||||
cardType: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.optional();
|
||||
|
||||
const postCollection = defineCollection({
|
||||
schema: z.object({
|
||||
title: z.string(),
|
||||
description: z.string().optional(),
|
||||
image: z.string().optional(),
|
||||
|
||||
canonical: z.string().url().optional(),
|
||||
|
||||
publishDate: z.date().or(z.string()).optional(),
|
||||
publishDate: z.date().optional(),
|
||||
updateDate: z.date().optional(),
|
||||
draft: z.boolean().optional(),
|
||||
|
||||
title: z.string(),
|
||||
excerpt: z.string().optional(),
|
||||
image: z.string().optional(),
|
||||
|
||||
category: z.string().optional(),
|
||||
tags: z.array(z.string()).optional(),
|
||||
author: z.string().optional(),
|
||||
|
||||
metadata: metadataDefinition(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const collections = {
|
||||
post: post,
|
||||
post: postCollection,
|
||||
};
|
||||
|
@ -1,7 +1,6 @@
|
||||
---
|
||||
publishDate: 2023-07-17T00:00:00Z
|
||||
title: AstroWind template in depth
|
||||
description: Internals documentation
|
||||
excerpt: While easy to get started, Astrowind is quite complex internally. This page provides documentation on some of the more intricate parts.
|
||||
image: ~/assets/images/stickers.jpg
|
||||
category: Documentation
|
||||
@ -9,7 +8,8 @@ tags:
|
||||
- astro
|
||||
- tailwind css
|
||||
- front-end
|
||||
canonical: https://astrowind.vercel.app/astrowind-template-in-depth
|
||||
metadata:
|
||||
canonical: https://astrowind.vercel.app/astrowind-template-in-depth
|
||||
---
|
||||
|
||||
import DListItem from '~/components/ui/DListItem.astro';
|
||||
|
@ -1,14 +1,14 @@
|
||||
---
|
||||
publishDate: 2023-01-12T00:00:00Z
|
||||
title: Get started with AstroWind to create a website using Astro and Tailwind CSS
|
||||
description: Lorem ipsum dolor sit amet
|
||||
excerpt: Sint sit cillum pariatur eiusmod nulla pariatur ipsum. Sit laborum anim qui mollit tempor pariatur.
|
||||
image: ~/assets/images/do-more.jpg
|
||||
category: Tutorials
|
||||
tags:
|
||||
- astro
|
||||
- tailwind css
|
||||
canonical: https://astrowind.vercel.app/get-started-website-with-astro-tailwind-css
|
||||
metadata:
|
||||
canonical: https://astrowind.vercel.app/get-started-website-with-astro-tailwind-css
|
||||
---
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
|
@ -7,7 +7,8 @@ tags:
|
||||
- astro
|
||||
- tailwind css
|
||||
- theme
|
||||
canonical: https://astrowind.vercel.app/how-to-customize-astrowind-to-your-brand
|
||||
metadata:
|
||||
canonical: https://astrowind.vercel.app/how-to-customize-astrowind-to-your-brand
|
||||
---
|
||||
|
||||
## Congue justo vulputate nascetur convallis varius orci fringilla nulla pharetr
|
||||
|
@ -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 = {
|
||||
title: `Category'${category}' ${currentPage > 1 ? ` — Page ${currentPage}` : ''}`,
|
||||
description: SITE.description,
|
||||
noindex: BLOG?.category?.noindex,
|
||||
|
||||
const metadata = {
|
||||
title: `Category '${category}' ${currentPage > 1 ? ` — Page ${currentPage}` : ''}`,
|
||||
robots: {
|
||||
index: blogCategoryRobots?.index,
|
||||
follow: blogCategoryRobots?.follow,
|
||||
},
|
||||
};
|
||||
---
|
||||
|
||||
<Layout meta={meta}>
|
||||
<section class="px-6 sm:px-6 py-12 sm:py-16 lg:py-20 mx-auto max-w-4xl">
|
||||
<Layout metadata={metadata}>
|
||||
<section class="px-4 md:px-6 py-12 sm:py-16 lg:py-20 mx-auto max-w-4xl">
|
||||
<Headline><span class="capitalize">{category.replaceAll('-', ' ')}</span></Headline>
|
||||
<BlogList posts={page.data} />
|
||||
<Pagination prevUrl={page.url.prev} nextUrl={page.url.next} />
|
||||
|
@ -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 = {
|
||||
title: post.title,
|
||||
description: post.description,
|
||||
canonical: post.canonical || url,
|
||||
image: await findImage(post.image),
|
||||
noindex: BLOG?.post?.noindex,
|
||||
ogType: 'article',
|
||||
};
|
||||
const url = getCanonical(getPermalink(post.permalink, 'post'));
|
||||
const image = (await findImage(post.image)) as ImageMetadata | undefined;
|
||||
|
||||
const metadata = merge(
|
||||
{
|
||||
title: post.title,
|
||||
description: post.excerpt,
|
||||
robots: {
|
||||
index: blogPostRobots?.index,
|
||||
follow: blogPostRobots?.follow,
|
||||
},
|
||||
openGraph: {
|
||||
type: 'article',
|
||||
...(image ? { images: [{ url: image?.src, width: image?.width, height: image?.height }] } : {}),
|
||||
},
|
||||
},
|
||||
{ ...(post?.metadata ? { ...post.metadata, canonical: post.metadata?.canonical || url } : {}) }
|
||||
);
|
||||
---
|
||||
|
||||
<Layout {meta}>
|
||||
<SinglePost post={{ ...post, image: meta.image }} url={url} />
|
||||
<Layout metadata={metadata}>
|
||||
<SinglePost post={{ ...post, image: image }} url={url} />
|
||||
<ToBlogLink />
|
||||
</Layout>
|
||||
|
@ -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 getStaticPathsBlogPost = () => async () => {
|
||||
if (!isBlogEnabled || !isBlogPostRouteEnabled) return [];
|
||||
return (await fetchPosts()).map((post) => ({
|
||||
params: {
|
||||
blog: post.permalink,
|
||||
},
|
||||
props: { post },
|
||||
}));
|
||||
};
|
||||
|
||||
/** */
|
||||
export const findCategories = async (): Promise<Array<string>> => {
|
||||
const posts = await fetchPosts();
|
||||
const categories = posts.reduce((acc, post: Post) => {
|
||||
if (post.category) {
|
||||
return [...acc, post.category];
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
return [...new Set(categories)];
|
||||
};
|
||||
export const getStaticPathsBlogCategory =
|
||||
() =>
|
||||
async ({ paginate }) => {
|
||||
if (!isBlogEnabled || !isBlogCategoryRouteEnabled) return [];
|
||||
|
||||
const posts = await fetchPosts();
|
||||
const categories = new Set();
|
||||
posts.map((post) => {
|
||||
typeof post.category === 'string' && categories.add(post.category.toLowerCase());
|
||||
});
|
||||
|
||||
return Array.from(categories).map((category: string) =>
|
||||
paginate(
|
||||
posts.filter((post) => typeof post.category === 'string' && category === post.category.toLowerCase()),
|
||||
{
|
||||
params: { category: category, blog: CATEGORY_BASE || undefined },
|
||||
pageSize: blogPostsPerPage,
|
||||
props: { category },
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
/** */
|
||||
export const getStaticPathsBlogTag =
|
||||
() =>
|
||||
async ({ paginate }) => {
|
||||
if (!isBlogEnabled || !isBlogTagRouteEnabled) return [];
|
||||
|
||||
const posts = await fetchPosts();
|
||||
const tags = new Set();
|
||||
posts.map((post) => {
|
||||
Array.isArray(post.tags) && post.tags.map((tag) => tags.add(tag.toLowerCase()));
|
||||
});
|
||||
|
||||
return Array.from(tags).map((tag: string) =>
|
||||
paginate(
|
||||
posts.filter((post) => Array.isArray(post.tags) && post.tags.find((elem) => elem.toLowerCase() === tag)),
|
||||
{
|
||||
params: { tag: tag, blog: TAG_BASE || undefined },
|
||||
pageSize: blogPostsPerPage,
|
||||
props: { tag },
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
|
@ -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