Mastodon post on homepage

This commit is contained in:
2025-08-30 19:38:12 -04:00
parent 26d9288aa4
commit 67e2229b82
12 changed files with 278 additions and 12 deletions
+32
View File
@@ -0,0 +1,32 @@
{
"$ref": "#/definitions/blog",
"definitions": {
"blog": {
"type": "object",
"properties": {
"title": {
"type": "string"
},
"pubDate": {
"type": "string"
},
"tags": {
"type": "array",
"items": {
"type": "string"
}
},
"$schema": {
"type": "string"
}
},
"required": [
"title",
"pubDate",
"tags"
],
"additionalProperties": false
}
},
"$schema": "http://json-schema.org/draft-07/schema#"
}
+218
View File
@@ -0,0 +1,218 @@
declare module 'astro:content' {
interface Render {
'.mdx': Promise<{
Content: import('astro').MarkdownInstance<{}>['Content'];
headings: import('astro').MarkdownHeading[];
remarkPluginFrontmatter: Record<string, any>;
components: import('astro').MDXInstance<{}>['components'];
}>;
}
}
declare module 'astro:content' {
export interface RenderResult {
Content: import('astro/runtime/server/index.js').AstroComponentFactory;
headings: import('astro').MarkdownHeading[];
remarkPluginFrontmatter: Record<string, any>;
}
interface Render {
'.md': Promise<RenderResult>;
}
export interface RenderedContent {
html: string;
metadata?: {
imagePaths: Array<string>;
[key: string]: unknown;
};
}
}
declare module 'astro:content' {
type Flatten<T> = T extends { [K: string]: infer U } ? U : never;
export type CollectionKey = keyof AnyEntryMap;
export type CollectionEntry<C extends CollectionKey> = Flatten<AnyEntryMap[C]>;
export type ContentCollectionKey = keyof ContentEntryMap;
export type DataCollectionKey = keyof DataEntryMap;
type AllValuesOf<T> = T extends any ? T[keyof T] : never;
type ValidContentEntrySlug<C extends keyof ContentEntryMap> = AllValuesOf<
ContentEntryMap[C]
>['slug'];
export type ReferenceDataEntry<
C extends CollectionKey,
E extends keyof DataEntryMap[C] = string,
> = {
collection: C;
id: E;
};
export type ReferenceContentEntry<
C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}) = string,
> = {
collection: C;
slug: E;
};
export type ReferenceLiveEntry<C extends keyof LiveContentConfig['collections']> = {
collection: C;
id: string;
};
/** @deprecated Use `getEntry` instead. */
export function getEntryBySlug<
C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}),
>(
collection: C,
// Note that this has to accept a regular string too, for SSR
entrySlug: E,
): E extends ValidContentEntrySlug<C>
? Promise<CollectionEntry<C>>
: Promise<CollectionEntry<C> | undefined>;
/** @deprecated Use `getEntry` instead. */
export function getDataEntryById<C extends keyof DataEntryMap, E extends keyof DataEntryMap[C]>(
collection: C,
entryId: E,
): Promise<CollectionEntry<C>>;
export function getCollection<C extends keyof AnyEntryMap, E extends CollectionEntry<C>>(
collection: C,
filter?: (entry: CollectionEntry<C>) => entry is E,
): Promise<E[]>;
export function getCollection<C extends keyof AnyEntryMap>(
collection: C,
filter?: (entry: CollectionEntry<C>) => unknown,
): Promise<CollectionEntry<C>[]>;
export function getLiveCollection<C extends keyof LiveContentConfig['collections']>(
collection: C,
filter?: LiveLoaderCollectionFilterType<C>,
): Promise<
import('astro').LiveDataCollectionResult<LiveLoaderDataType<C>, LiveLoaderErrorType<C>>
>;
export function getEntry<
C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}),
>(
entry: ReferenceContentEntry<C, E>,
): E extends ValidContentEntrySlug<C>
? Promise<CollectionEntry<C>>
: Promise<CollectionEntry<C> | undefined>;
export function getEntry<
C extends keyof DataEntryMap,
E extends keyof DataEntryMap[C] | (string & {}),
>(
entry: ReferenceDataEntry<C, E>,
): E extends keyof DataEntryMap[C]
? Promise<DataEntryMap[C][E]>
: Promise<CollectionEntry<C> | undefined>;
export function getEntry<
C extends keyof ContentEntryMap,
E extends ValidContentEntrySlug<C> | (string & {}),
>(
collection: C,
slug: E,
): E extends ValidContentEntrySlug<C>
? Promise<CollectionEntry<C>>
: Promise<CollectionEntry<C> | undefined>;
export function getEntry<
C extends keyof DataEntryMap,
E extends keyof DataEntryMap[C] | (string & {}),
>(
collection: C,
id: E,
): E extends keyof DataEntryMap[C]
? string extends keyof DataEntryMap[C]
? Promise<DataEntryMap[C][E]> | undefined
: Promise<DataEntryMap[C][E]>
: Promise<CollectionEntry<C> | undefined>;
export function getLiveEntry<C extends keyof LiveContentConfig['collections']>(
collection: C,
filter: string | LiveLoaderEntryFilterType<C>,
): Promise<import('astro').LiveDataEntryResult<LiveLoaderDataType<C>, LiveLoaderErrorType<C>>>;
/** Resolve an array of entry references from the same collection */
export function getEntries<C extends keyof ContentEntryMap>(
entries: ReferenceContentEntry<C, ValidContentEntrySlug<C>>[],
): Promise<CollectionEntry<C>[]>;
export function getEntries<C extends keyof DataEntryMap>(
entries: ReferenceDataEntry<C, keyof DataEntryMap[C]>[],
): Promise<CollectionEntry<C>[]>;
export function render<C extends keyof AnyEntryMap>(
entry: AnyEntryMap[C][string],
): Promise<RenderResult>;
export function reference<C extends keyof AnyEntryMap>(
collection: C,
): import('astro/zod').ZodEffects<
import('astro/zod').ZodString,
C extends keyof ContentEntryMap
? ReferenceContentEntry<C, ValidContentEntrySlug<C>>
: ReferenceDataEntry<C, keyof DataEntryMap[C]>
>;
// Allow generic `string` to avoid excessive type errors in the config
// if `dev` is not running to update as you edit.
// Invalid collection names will be caught at build time.
export function reference<C extends string>(
collection: C,
): import('astro/zod').ZodEffects<import('astro/zod').ZodString, never>;
type ReturnTypeOrOriginal<T> = T extends (...args: any[]) => infer R ? R : T;
type InferEntrySchema<C extends keyof AnyEntryMap> = import('astro/zod').infer<
ReturnTypeOrOriginal<Required<ContentConfig['collections'][C]>['schema']>
>;
type ContentEntryMap = {
};
type DataEntryMap = {
"blog": Record<string, {
id: string;
body?: string;
collection: "blog";
data: InferEntrySchema<"blog">;
rendered?: RenderedContent;
filePath?: string;
}>;
};
type AnyEntryMap = ContentEntryMap & DataEntryMap;
type ExtractLoaderTypes<T> = T extends import('astro/loaders').LiveLoader<
infer TData,
infer TEntryFilter,
infer TCollectionFilter,
infer TError
>
? { data: TData; entryFilter: TEntryFilter; collectionFilter: TCollectionFilter; error: TError }
: { data: never; entryFilter: never; collectionFilter: never; error: never };
type ExtractDataType<T> = ExtractLoaderTypes<T>['data'];
type ExtractEntryFilterType<T> = ExtractLoaderTypes<T>['entryFilter'];
type ExtractCollectionFilterType<T> = ExtractLoaderTypes<T>['collectionFilter'];
type ExtractErrorType<T> = ExtractLoaderTypes<T>['error'];
type LiveLoaderDataType<C extends keyof LiveContentConfig['collections']> =
LiveContentConfig['collections'][C]['schema'] extends undefined
? ExtractDataType<LiveContentConfig['collections'][C]['loader']>
: import('astro/zod').infer<
Exclude<LiveContentConfig['collections'][C]['schema'], undefined>
>;
type LiveLoaderEntryFilterType<C extends keyof LiveContentConfig['collections']> =
ExtractEntryFilterType<LiveContentConfig['collections'][C]['loader']>;
type LiveLoaderCollectionFilterType<C extends keyof LiveContentConfig['collections']> =
ExtractCollectionFilterType<LiveContentConfig['collections'][C]['loader']>;
type LiveLoaderErrorType<C extends keyof LiveContentConfig['collections']> = ExtractErrorType<
LiveContentConfig['collections'][C]['loader']
>;
export type ContentConfig = typeof import("../src/content.config.js");
export type LiveContentConfig = never;
}
File diff suppressed because one or more lines are too long
+1
View File
@@ -1 +1,2 @@
/// <reference types="astro/client" />
/// <reference path="content.d.ts" />
+1
View File
@@ -9,6 +9,7 @@ import alpinejs from '@astrojs/alpinejs';
export default defineConfig({
site: 'https://ghall.space',
output: 'static',
integrations: [mdx(), alpinejs()],
redirects: {
'/posts/[...slug]': '/blog/[...slug]',
+6 -4
View File
@@ -13,9 +13,7 @@ const { title, date, slug } = Astro.props;
---
<div class="blog-header">
<h2>
{slug ? <a href={`/blog/${slug}`}>{title}</a> : title}
</h2>
{slug ? <a href={`/blog/${slug}`}>{title}</a> : <h1>{title}</h1>}
<div>
<CalendarIcon class="calendar-icon" width={24} height={24} />
<strong>{format(add(new Date(date), { hours: 6 }), 'MMM do, y')}</strong>
@@ -23,11 +21,15 @@ const { title, date, slug } = Astro.props;
</div>
<style>
a {
font-size: 1.3rem;
}
.calendar-icon {
transform: translateY(0.3rem);
}
strong {
font-weight: bold;
font-weight: bolder;
}
</style>
+10 -2
View File
@@ -5,7 +5,7 @@ import Drawer from './Drawer.astro';
<header>
<div class="container">
<h1><a href="/">ghall.space</a></h1>
<a href="/">ghall.space</a>
<Nav />
<Drawer />
</div>
@@ -21,6 +21,7 @@ import Drawer from './Drawer.astro';
top: 0;
position: fixed;
z-index: 1;
border-bottom: var(--border);
}
.container {
@@ -34,8 +35,15 @@ import Drawer from './Drawer.astro';
height: 100%;
}
h1 > a {
a {
font-size: 1.6rem;
color: var(--text);
font-weight: bold;
}
@media (prefers-color-scheme: dark) {
header {
background-color: rgba(30, 30, 46, 90%);
}
}
</style>
+1 -1
View File
@@ -26,6 +26,7 @@ const pathComponents = pathname.split('/').slice(1);
list-style: none;
gap: 20px;
font-size: 1.15rem;
font-weight: 600;
}
a:hover {
@@ -33,7 +34,6 @@ const pathComponents = pathname.split('/').slice(1);
}
.selected {
font-weight: bold;
}
.underline {
-1
View File
@@ -13,7 +13,6 @@
this.latestPost = {
link: item.querySelector('link').textContent,
body: item.querySelector('description').textContent,
};
})
.catch((error) => {
@@ -1,6 +1,8 @@
import { z, defineCollection } from 'astro:content';
import { glob } from 'astro/loaders';
const blogCollection = defineCollection({
loader: glob({ pattern: '**\/*.md', base: './src/content/blog' }),
schema: z.object({
title: z.string(),
pubDate: z.string().transform((str: string) => new Date(str)),
+2 -2
View File
@@ -19,7 +19,7 @@
--background: #fcfcfc;
--radius: 5px;
--shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px;
--border: 1px solid #8c8fa1;
--border: 1px solid #ced3f1;
--max-page-width: 800px;
}
@@ -31,7 +31,7 @@
--orange: #fab387;
--text: #cdd6f4;
--background: #1e1e2e;
--border: 1px solid rgb(131, 139, 167);
--border: 1px solid #505160;
}
}
+4 -1
View File
@@ -5,6 +5,7 @@ import { getCollection } from 'astro:content';
import Layout from '@layouts/Layout.astro';
import Avatar from '@components/Avatar.astro';
import LatestPost from '@components/LatestPost.astro';
import MastodonIcon from '../assets/svg/mastodon.svg';
import BlueskyIcon from '../assets/svg/bluesky.svg';
@@ -19,6 +20,7 @@ const latestPost = posts.sort(
---
<Layout title="Welcome">
<Avatar size={200} />
<p>
My name is <strong>Graham</strong> (he/him), a full-stack web developer, and
tech enthusiast.
@@ -61,10 +63,11 @@ const latestPost = posts.sort(
target="_blank">Natalia Vazquez</a
>.
</p>
<LatestPost />
</Layout>
<style>
a > svg {
transform: translateY(0.18rem);
transform: translateY(0.1rem);
}
</style>