Skip to content
Code Inside Out

Add-ons for Astro site

Tags: linuxnodejs

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:

src/components/RecentPosts.astro
---
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:

tsconfig.json
{
"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:

src/components/ClerkProfile.astro
---
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/.

src/components/SiteTitleWithClerkProfile.astro
---
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:

astro.config.mjs
export default defineConfig({
integrations: [
starlight({
components: {
SiteTitle: './src/components/SiteTitleWithClerkProfile.astro',
},

Post Info


Create a component:

src/components/PostInfo.astro
---
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:

astro.config.mjs
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


src/middleware.ts
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:

src/middleware.ts
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:

src/env.d.ts
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:

node_modules/@clerk/types/dist/index.d.ts
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:

src/middleware.ts
declare global {
interface UserPublicMetadata {
premium: boolean | undefined;
}
}

Finally, Locals.premium is avaible in Astro files:

src/components/Premium.astro
---
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:

src/content/protected/sample.mdx
---
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:

src/pages/protected/[slug].astro
---
import { getEntry } from "astro:content";
// 1. Get the slug from the incoming server request
const { slug } = Astro.params;
if (slug === undefined) {
throw new Error("Slug is required");
}
// 2. Query for the entry directly using the requested slug
const entry = await getEntry("protected", slug);
// 3. Redirect if the entry does not exist
if (entry === undefined) {
return Astro.redirect("/404");
}
// 4. (Optional) Render the entry to HTML in the template
const { Content } = await entry.render();
// 5. (Optional) Set prerender flag if using Hybrid mode
export const prerender = false;
---
<Content />

Query pages in a collection:

src/pages/protected-pages.astro
---
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>