Add-ons for Astro site
Here come some user components or modifications for adding features on Astro site.
User Components
Adding new tweaks into Astro framework.
Recent Posts
Create a component:
---import { CardGrid, LinkCard } from "@astrojs/starlight/components";
let allPosts = await Astro.glob("../content/docs/blog/**/[!^_]*.mdx");allPosts = allPosts.filter((post) => post.frontmatter.slug);allPosts.sort((a, b) => a.frontmatter.date > b.frontmatter.date ? -1 : a.frontmatter.date < b.frontmatter.date ? 1 : 0);---
<CardGrid>{ allPosts .slice(0, 8) .map((post) => ( <LinkCard title={post.frontmatter.title} description={post.frontmatter.excerpt} href={post.frontmatter.slug} /> ))}</CardGrid>
Declare alias path for user components:
{ "extends": "astro/tsconfigs/strict", "compilerOptions": { "baseUrl": ".", "paths": { "@components/*": ["src/components/*"], "@assets/*": ["src/assets/*"] } }}
Use it in posts:
import RecentPosts from '@components/RecentPosts.astro';
<RecentPosts />
Clerk Profile
Create a component:
---import { SignedIn, SignedOut, UserButton } from "@clerk/astro/components";import { Icon } from '@astrojs/starlight/components';---
<div class="sl-flex"> <SignedOut> <a class="clerk sl-flex" href="/signin"> <Icon name="github" size="1.5rem" color="var(--sl-color-text-accent)"/> </a> </SignedOut> <SignedIn> <div class="clerk sl-flex"> <UserButton /> </div> </SignedIn></div>
<style> clerk-signed-in, clerk-signed-out { align-content: center; } .clerk { margin-inline-start: 0.5rem; }</style>
Site Title with Clerk Profile
Refer: https://starlight.astro.build/guides/overriding-components/.
---import type { Props } from '@astrojs/starlight/props';import Default from '@astrojs/starlight/components/SiteTitle.astro';import Clerk from './ClerkProfile.astro';---
<Default {...Astro.props}><slot /></Default><Clerk />
Then tell Starlight to use new component instead the old one:
export default defineConfig({ integrations: [ starlight({ components: { SiteTitle: './src/components/SiteTitleWithClerkProfile.astro', },
Post Info
Create a component:
---const frontmatter = Astro.props.frontmatter;---
<div> { frontmatter.tags.length > 0 ? ( <p> {Astro.locals.t('starlightBlog.post.tags')} {frontmatter.tags.map((tag: string) => ( <a href={"/blog/tags/" + tag} style=" border: 1px solid var(--sl-color-gray-5); border-radius: 0.3rem; font-size: var(--sl-text-sm); margin-inline: 0.2rem; padding: 0.25rem 0.5rem 0.35rem; text-decoration: none;" >{tag}</a> ))} </p> ) : null } <p>{frontmatter.excerpt}</p></div>
Then use it in posts:
import PostInfo from '@components/PostInfo.astro';
<PostInfo frontmatter={frontmatter} />
Project Configurations
When using some Starlight plugins, note the integration order:
export default defineConfig({ integrations: [ starlight({ components: { SiteTitle: './src/components/SiteTitleWithClerkProfile.astro', }, plugins: [ starlightImageZoom(), // apply content modifier first starlightThemeRapide(), // then apply theme starlightViewModes(), // then apply view modes starlightBlog(), // then apply features starlightLinksValidator({ // for production build errorOnLocalLinks: false, }), ],
Middleware
Async method
export const onRequest = clerkMiddleware(async (auth, context, next) => { const data = await loadSomething();});
Use context.locals
Refer: https://docs.astro.build/en/guides/middleware/#storing-data-in-contextlocals.
Using Typescript, some datatype need to be declared to make build done.
For example, below method fails to know premium
property:
export const onRequest = clerkMiddleware(async (auth, context, next) => { onst user = await context.locals.currentUser(); context.locals.premium = user?.publicMetadata.premium; return next();});
src/middleware.ts:20:30 - error ts(2339): Property 'premium' does not exist on type 'Locals'.
To fix this, declare property premium
for App.Locals
:
declare namespace App { interface Locals { premium: boolean | undefined; }}
Now, there is mismatch type between Locals.premium
and UserPublicMetadata.premium
.
Clerk defines UserPublicMetadata’s properties as unknown type:
declare global { /** * If you want to provide custom types for the user.publicMetadata object, * simply redeclare this rule in the global namespace. * Every user object will use the provided type. */ interface UserPublicMetadata { [k: string]: unknown; }
So, just re-declare it:
declare global { interface UserPublicMetadata { premium: boolean | undefined; }}
Finally, Locals.premium
is avaible in Astro files:
---const premium = Astro.locals.premium;---
{ premium ? <slot /> : <p style="color:red;"> Protected Content! Please <a href="/signin">sign in</a> to view the content. </p>}
Dynamic Route for Protected Pages
A content collection is any top-level directory inside the reserved src/content project directory, such as src/content/docs
and src/content/protected
.
If a collection needs a schema, it should be defined in src/content/config.ts
.
Create a sample post:
---title: Sample---
export const prerender = false;import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";import Premium from '@components/Premium.astro'
<StarlightPage frontmatter={frontmatter}><Premium>
Sample content.
</Premium></StarlightPage>"
Content collections are stored outside of the src/pages
directory.
This means that no routes are generated for collection items by default.
Create dynamic route:
---import { getEntry } from "astro:content";
// 1. Get the slug from the incoming server requestconst { slug } = Astro.params;if (slug === undefined) { throw new Error("Slug is required");}
// 2. Query for the entry directly using the requested slugconst entry = await getEntry("protected", slug);
// 3. Redirect if the entry does not existif (entry === undefined) { return Astro.redirect("/404");}
// 4. (Optional) Render the entry to HTML in the templateconst { Content } = await entry.render();
// 5. (Optional) Set prerender flag if using Hybrid modeexport const prerender = false;---
<Content />
Query pages in a collection:
---import StarlightPage from "@astrojs/starlight/components/StarlightPage.astro";import { CardGrid, LinkCard } from "@astrojs/starlight/components";import Premium from "@components/Premium.astro"
import { getCollection, getEntry } from 'astro:content';const allProtectedPages = await getCollection('protected');
export const prerender = false;---
<StarlightPage frontmatter={{ title: "Protected Pages", excerpt: "Protected Pages contains sentisive data, so they are only accessible to users who have been granted with a proper permission."}}><Premium>{ allProtectedPages.map(page => ( <LinkCard title={page.data.title} description={page.data.excerpt} href={"protected/" + page.slug} /> ))}</Premium></StarlightPage>