Compare commits
10 Commits
485e3a0a91
...
astro
| Author | SHA1 | Date | |
|---|---|---|---|
| 67779ecd40 | |||
| eda7d3aa00 | |||
| e0af57e5b1 | |||
| 675ef1003d | |||
| 68bb25357b | |||
| 3f24f42c1f | |||
| bfdaac8095 | |||
| e02af4c552 | |||
| fd8ed38572 | |||
| 5ffb51d5ce |
@@ -1,6 +1,7 @@
|
||||
import { defineConfig } from "astro/config";
|
||||
import mdx from '@astrojs/mdx';
|
||||
import vue from '@astrojs/vue';
|
||||
import { remarkDescription } from './src/plugins.ts';
|
||||
|
||||
export default defineConfig({
|
||||
integrations: [
|
||||
@@ -9,6 +10,7 @@ export default defineConfig({
|
||||
],
|
||||
prefetch: true,
|
||||
markdown: {
|
||||
remarkPlugins: [remarkDescription],
|
||||
shikiConfig: {
|
||||
themes: {
|
||||
light: 'dracula',
|
||||
|
||||
@@ -49,7 +49,7 @@ My word-search grid appears to be 140 characters by 140, so I'm just going to ha
|
||||
|
||||
Not gonna lie here, this part took me _way_ longer than I expected it to. See, the standard way to read a file in Fortran is with the `read()` statement. (It looks like a function call, but it's not.) You use it something like this:
|
||||
|
||||
```fortran
|
||||
```f90
|
||||
read(file_handle, *) somevar, anothervar, anothervar2
|
||||
```
|
||||
|
||||
@@ -57,7 +57,7 @@ Or at least, that's one way of using it. But here's the problem: by default, For
|
||||
|
||||
Initially, I thought I might be able to do this:
|
||||
|
||||
```fortran
|
||||
```f90
|
||||
character, dimension(140, 140) :: grid
|
||||
|
||||
! ...later
|
||||
@@ -70,7 +70,7 @@ But sadly, this kept spitting out errors about how it had encountered the end of
|
||||
|
||||
My next try looked something like this:
|
||||
|
||||
```fortran
|
||||
```f90
|
||||
do row = 1, 100
|
||||
read(file_handle, *) grid(row, :)
|
||||
end do
|
||||
@@ -82,7 +82,7 @@ I'm pretty sure the proper way to do this would be to figure out how to set the
|
||||
|
||||
So instead, I decided to just make it dumber.
|
||||
|
||||
```fortran
|
||||
```f90
|
||||
program advent04
|
||||
implicit none
|
||||
|
||||
@@ -139,7 +139,7 @@ S A M X M A S
|
||||
S . . S . . S
|
||||
```
|
||||
|
||||
Which has all 8 possible orientationS of the word `XMAS` starting from the central X. Then, we can just take a sliding "window" of the same size into our puzzle grid and compare it to the test grid. This is a native operation in Fortran--comparing two arrays of the same size results in a third array whose elements are the result of each individual comparison from the original arrays. Then we can just call `count()` on the resulting array to get the number of true values, and we know how many characters matched up. Subtract 1 for the central X we already knew about, then divide by 3 since there are 3 letters remaining in each occurrence of `XMAS`, and Bob's your uncle, right?
|
||||
Which has all 8 possible orientations of the word `XMAS` starting from the central X. Then, we can just take a sliding "window" of the same size into our puzzle grid and compare it to the test grid. This is a native operation in Fortran--comparing two arrays of the same size results in a third array whose elements are the result of each individual comparison from the original arrays. Then we can just call `count()` on the resulting array to get the number of true values, and we know how many characters matched up. Subtract 1 for the central X we already knew about, then divide by 3 since there are 3 letters remaining in each occurrence of `XMAS`, and Bob's your uncle, right?
|
||||
|
||||
...Wait, no. That won't work because it doesn't account for partial matches. Say we had a "window" that looked like this (I'm only showing the bottom-right quadrant of the window for simplicity):
|
||||
|
||||
@@ -164,7 +164,7 @@ Will this work? Is it even marginally more efficient than the stupidly obvious w
|
||||
|
||||
Ok, first things first. Let's adjust the data-loading code to pad the grid with 3 bogus values on each edge, so that we can still generate our window correctly when we're looking at a point near the edge of the grid.
|
||||
|
||||
```fortran
|
||||
```f90
|
||||
grid = '.' ! probably wouldn't matter if we skipped this, uninitialized memory just makes me nervous
|
||||
|
||||
open(newunit=handle, file="data/04.txt", status="old", action="read")
|
||||
@@ -196,7 +196,7 @@ Oh. Oh, _noooo_.
|
||||
|
||||
It's okay, I mean, uh, it's not _that_ much higher. Only two orders of magnitude, and what are the odds of all eight versions of `XMAS` appearing in the same window, anyway? Something like 1/4<sup>25</sup>? Maybe we can still make this work.
|
||||
|
||||
```fortran
|
||||
```f90
|
||||
integer function count_xmas(row, col) result(count)
|
||||
implicit none
|
||||
|
||||
@@ -277,7 +277,7 @@ Those `&`s are line-continuation characters, by the way. Apparently you can't ha
|
||||
|
||||
Now we just have to put it all together:
|
||||
|
||||
```fortran
|
||||
```f90
|
||||
total = 0
|
||||
do col = 4, 143
|
||||
do row = 4, 143
|
||||
@@ -314,7 +314,7 @@ Hmm, I wonder if there's a way to take a single starting test grid and manipulat
|
||||
|
||||
Turns out, yes! Yes there is. We can use a combination of slicing with a negative step, and transposing, which switches rows with columns, effectively rotating and flipping the array. So setting up our test grids looks like this:
|
||||
|
||||
```fortran
|
||||
```f90
|
||||
character, dimension(3, 3) :: window, t1, t2, t3, t4
|
||||
|
||||
t1 = reshape( &
|
||||
@@ -332,7 +332,7 @@ t4 = t3(:, 3:1:-1) ! flip t3 left-to-right
|
||||
|
||||
Then we can just compare the window to each test grid:
|
||||
|
||||
```fortran
|
||||
```f90
|
||||
window = grid(row - 1:row + 1, col - 1:col + 1)
|
||||
if ( &
|
||||
count_matches(window, t1) == 5 &
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
title: Example after post
|
||||
date: 2026-02-28
|
||||
draft: true
|
||||
---
|
||||
|
||||
## After
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
---
|
||||
export interface Props {
|
||||
name: string,
|
||||
width?: string,
|
||||
height?: string,
|
||||
display?: string,
|
||||
};
|
||||
|
||||
const { name } = Astro.props;
|
||||
const { name, width, height, display } = Astro.props;
|
||||
|
||||
const icons = import.meta.glob<{string: string}>('@components/icons/*.svg', { query: '?raw', import: 'default' });
|
||||
const path = `/src/components/icons/${name}.svg`;
|
||||
@@ -13,11 +16,12 @@ if (icons[path] === undefined) {
|
||||
const icon = await icons[path]();
|
||||
---
|
||||
|
||||
<Fragment set:html={icon} />
|
||||
<span class="icon" set:html={icon} />
|
||||
|
||||
<style>
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
<style define:vars={{ width, height, display }}>
|
||||
.icon :global(svg) {
|
||||
display: var(--display, block);
|
||||
width: var(--width, 100%);
|
||||
height: var(--height, 100%);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,16 +8,54 @@ import { render } from 'astro:content';
|
||||
import Toc from '@components/Toc.vue';
|
||||
import { formatDate } from '@lib/datefmt';
|
||||
|
||||
import Icon from '@components/Icon.astro';
|
||||
import { headingElements } from '@components/headings';
|
||||
|
||||
export interface Props {
|
||||
entry: CollectionEntry<'posts'>,
|
||||
prevSlug: string | null,
|
||||
nextSlug: string | null,
|
||||
prevSlug?: string | null,
|
||||
nextSlug?: string | null,
|
||||
};
|
||||
|
||||
const { entry, prevSlug, nextSlug } = Astro.props;
|
||||
const { Content, headings } = await render(entry);
|
||||
---
|
||||
|
||||
<article class="prose" data-dropcap-style={entry.data.dropcap}>
|
||||
<header class="title">
|
||||
<h1>
|
||||
<!-- <SmallCaps text={entry.data.title} upperWeight={500} lowerWeight={800} /> -->
|
||||
{ entry.data.title }
|
||||
</h1>
|
||||
<p class="subtitle">{ formatDate(entry.data.date) }</p>
|
||||
</header>
|
||||
|
||||
<div id="left-gutter">
|
||||
<Toc client:load {headings} />
|
||||
</div>
|
||||
|
||||
<section class="post">
|
||||
<Content components={headingElements} />
|
||||
</section>
|
||||
|
||||
<div id="right-gutter" />
|
||||
|
||||
<footer>
|
||||
{prevSlug && (
|
||||
<div class="footer-link left">
|
||||
<Icon name="arrow-left" height="1em" />
|
||||
<a href={`/${prevSlug}`} data-astro-prefetch>Older</a>
|
||||
</div>
|
||||
)}
|
||||
{nextSlug && (
|
||||
<div class="footer-link right">
|
||||
<a href={`/${nextSlug}`} data-astro-prefetch>Newer</a>
|
||||
<Icon name="arrow-right" height="1em" />
|
||||
</div>
|
||||
)}
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
<style>
|
||||
/* 3-column grid: left gutter, center content, and right gutter */
|
||||
article {
|
||||
@@ -59,7 +97,11 @@ footer {
|
||||
grid-column: 2 / 3;
|
||||
margin-bottom: 2.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
color: var(--content-color-faded);
|
||||
margin-top: 1.5rem;
|
||||
padding-top: 2.25rem;
|
||||
border-top: 1px solid var(--content-color-faded);
|
||||
|
||||
& a {
|
||||
font-size: 1.25rem;
|
||||
@@ -77,6 +119,32 @@ footer {
|
||||
}
|
||||
}
|
||||
|
||||
/* basic styles */
|
||||
.footer-link {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
align-items: center;
|
||||
|
||||
&.right {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* animate on hover */
|
||||
.footer-link {
|
||||
& :global(svg) {
|
||||
transition: transform 150ms;
|
||||
}
|
||||
|
||||
&.right:hover :global(svg) {
|
||||
transform: translateX(50%);
|
||||
}
|
||||
|
||||
&.left:hover :global(svg) {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
article {
|
||||
& :global(section.post::first-letter) {
|
||||
font-family: 'Baskervville';
|
||||
@@ -94,32 +162,3 @@ article {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<article class="prose" data-dropcap-style={entry.data.dropcap}>
|
||||
<header class="title">
|
||||
<h1>
|
||||
<!-- <SmallCaps text={entry.data.title} upperWeight={500} lowerWeight={800} /> -->
|
||||
{ entry.data.title }
|
||||
</h1>
|
||||
<p class="subtitle">{ formatDate(entry.data.date) }</p>
|
||||
</header>
|
||||
|
||||
<div id="left-gutter">
|
||||
<Toc client:load {headings} />
|
||||
</div>
|
||||
|
||||
<section class="post">
|
||||
<Content />
|
||||
</section>
|
||||
|
||||
<div id="right-gutter" />
|
||||
|
||||
<footer>
|
||||
{prevSlug && (
|
||||
<a href={`/${prevSlug}`} data-astro-prefetch>Older</a>
|
||||
)}
|
||||
{nextSlug && (
|
||||
<a href={`/${nextSlug}`} data-astro-prefetch>Newer</a>
|
||||
)}
|
||||
</footer>
|
||||
</article>
|
||||
|
||||
@@ -38,26 +38,15 @@ button {
|
||||
}
|
||||
|
||||
/* hide by default, i.e. if JS isn't enabled and the data-theme attribute didn't get set, */
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
transition:
|
||||
color 0.2s ease,
|
||||
opacity 0.5s ease,
|
||||
transform 0.5s ease;
|
||||
display: none;
|
||||
}
|
||||
|
||||
:global(html[data-theme="light"]) button#switch-to-dark {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: rotate(360deg);
|
||||
/* whichever one is currently active should be on top */
|
||||
z-index: 10;
|
||||
display: block;
|
||||
}
|
||||
|
||||
:global(html[data-theme="dark"]) button#switch-to-light {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transform: rotate(-360deg);
|
||||
z-index: 10;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
7
src/components/headings/H1.astro
Normal file
7
src/components/headings/H1.astro
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
import Heading from './Heading.astro';
|
||||
---
|
||||
|
||||
<Heading tag="h1" {...Astro.props}>
|
||||
<slot />
|
||||
</Heading>
|
||||
7
src/components/headings/H2.astro
Normal file
7
src/components/headings/H2.astro
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
import Heading from './Heading.astro';
|
||||
---
|
||||
|
||||
<Heading tag="h2" {...Astro.props}>
|
||||
<slot />
|
||||
</Heading>
|
||||
7
src/components/headings/H3.astro
Normal file
7
src/components/headings/H3.astro
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
import Heading from './Heading.astro';
|
||||
---
|
||||
|
||||
<Heading tag="h3" {...Astro.props}>
|
||||
<slot />
|
||||
</Heading>
|
||||
7
src/components/headings/H4.astro
Normal file
7
src/components/headings/H4.astro
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
import Heading from './Heading.astro';
|
||||
---
|
||||
|
||||
<Heading tag="h4" {...Astro.props}>
|
||||
<slot />
|
||||
</Heading>
|
||||
7
src/components/headings/H5.astro
Normal file
7
src/components/headings/H5.astro
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
import Heading from './Heading.astro';
|
||||
---
|
||||
|
||||
<Heading tag="h5" {...Astro.props}>
|
||||
<slot />
|
||||
</Heading>
|
||||
7
src/components/headings/H6.astro
Normal file
7
src/components/headings/H6.astro
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
import Heading from './Heading.astro';
|
||||
---
|
||||
|
||||
<Heading tag="h6" {...Astro.props}>
|
||||
<slot />
|
||||
</Heading>
|
||||
45
src/components/headings/Heading.astro
Normal file
45
src/components/headings/Heading.astro
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
import Icon from '@components/Icon.astro';
|
||||
|
||||
export interface Props extends astroHTML.JSX.HTMLAttributes {
|
||||
tag: string,
|
||||
}
|
||||
|
||||
const { tag: Tag, ...rest } = Astro.props;
|
||||
---
|
||||
|
||||
<Tag {...rest}>
|
||||
<slot />
|
||||
|
||||
<a href={`#${Astro.props.id}`} class="deep-link" >
|
||||
<Icon name="link" height="0.9em" width="0.9em" display="inline" />
|
||||
</a>
|
||||
</Tag>
|
||||
|
||||
<style>
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
position: relative;
|
||||
|
||||
&:hover :global(svg) {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
& :global(svg) {
|
||||
color: var(--content-color-faded);
|
||||
padding-bottom: 0.15em;
|
||||
}
|
||||
|
||||
&:hover :global(svg), &:active :global(svg) {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
@media(min-width: 60rem) {
|
||||
position: absolute;
|
||||
top: 0.1em;
|
||||
left: -1.25em;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
15
src/components/headings/index.ts
Normal file
15
src/components/headings/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import H1 from './H1.astro';
|
||||
import H2 from './H2.astro';
|
||||
import H3 from './H3.astro';
|
||||
import H4 from './H4.astro';
|
||||
import H5 from './H5.astro';
|
||||
import H6 from './H6.astro';
|
||||
|
||||
export const headingElements = {
|
||||
h1: H1,
|
||||
h2: H2,
|
||||
h3: H3,
|
||||
h4: H4,
|
||||
h5: H5,
|
||||
h6: H6,
|
||||
};
|
||||
3
src/components/icons/arrow-left.svg
Normal file
3
src/components/icons/arrow-left.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5 3 12m0 0 7.5-7.5M3 12h18" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 219 B |
3
src/components/icons/arrow-right.svg
Normal file
3
src/components/icons/arrow-right.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 219 B |
3
src/components/icons/link.svg
Normal file
3
src/components/icons/link.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 353 B |
@@ -1,10 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M21.752 15.002A9.72 9.72 0 0 1 18 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 0 0 3 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 0 0 9.002-5.998Z" />
|
||||
</svg>
|
||||
|
||||
<style>
|
||||
svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
Before Width: | Height: | Size: 425 B After Width: | Height: | Size: 364 B |
@@ -2,6 +2,12 @@
|
||||
import '@styles/main.css';
|
||||
import '@fontsource-variable/baskervville-sc';
|
||||
import ThemeSwitcher from '@components/ThemeSwitcher.astro';
|
||||
|
||||
export interface Props {
|
||||
pageTitle: string,
|
||||
}
|
||||
|
||||
const { pageTitle } = Astro.props;
|
||||
---
|
||||
|
||||
<html lang="en">
|
||||
@@ -9,6 +15,8 @@ import ThemeSwitcher from '@components/ThemeSwitcher.astro';
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
|
||||
<title>{pageTitle}</title>
|
||||
|
||||
<!-- avoid FOUC by setting the color schme here in the header -->
|
||||
<script>
|
||||
const explicitPref = localStorage.getItem('theme-preference');
|
||||
|
||||
10
src/lib/content.ts
Normal file
10
src/lib/content.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { getCollection } from 'astro:content';
|
||||
|
||||
|
||||
export async function listPosts() {
|
||||
// all posts in dev, exlucde draft posts in prod
|
||||
let entries = await getCollection('posts', ({ data }) => import.meta.env.DEV || !data.draft);
|
||||
// sort by date descending
|
||||
entries.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
|
||||
return entries;
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import { listPosts } from '@lib/content.ts';
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import Post from '@components/Post.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const entries = await getCollection('posts');
|
||||
entries.sort((a, b) => a.data.date.getTime() - b.data.date.getTime())
|
||||
const entries = await listPosts();
|
||||
|
||||
// for each route, the page gets passed the entry itself, plus the previous and next slugs
|
||||
// (if any), so that it can render links to them
|
||||
return entries.map((entry, idx) => {
|
||||
const prevSlug = entries[idx - 1]?.id || null;
|
||||
const nextSlug = entries[idx + 1]?.id || null;
|
||||
// entries are sorted in by date descending, so prev has a higher index and next has lower
|
||||
const prevSlug = entries[idx + 1]?.id || null;
|
||||
const nextSlug = entries[idx - 1]?.id || null;
|
||||
return {
|
||||
params: { slug: entry.id },
|
||||
props: { entry, prevSlug, nextSlug },
|
||||
@@ -20,8 +20,9 @@ export async function getStaticPaths() {
|
||||
|
||||
}
|
||||
|
||||
const { entry, prevSlug, nextSlug } = Astro.props;
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<Post {...Astro.props} />
|
||||
<BaseLayout pageTitle={entry.data.title}>
|
||||
<Post {entry} {prevSlug} {nextSlug} />
|
||||
</BaseLayout>
|
||||
|
||||
24
src/pages/about.astro
Normal file
24
src/pages/about.astro
Normal file
@@ -0,0 +1,24 @@
|
||||
---
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
---
|
||||
|
||||
<BaseLayout pageTitle="Joe's Blog - About">
|
||||
<article class="prose">
|
||||
<h1>About Me</h1>
|
||||
|
||||
<p>(Joe's wife wrote this because Joe feels weird writing about himself.)</p>
|
||||
|
||||
<p>Joe is a quirky, techy Tolkienite with a beautiful singing voice, an uncanny ability to do mental math, a bony, un-cuddleable frame, and a big mushy heart. He enjoys bike riding, computers, watching TV, reading about computers, playing Breath of the Wild, building computers, talking about something called "programming languages", and spending time with his family (which often involves fixing their computers). He graduated with a Liberal Arts degree from Thomas Aquinas College, the school of his forebears. He often remarks that he has greatly benefitted from the critical thinking skills he acquired at his alma mater in his current line of work.</p>
|
||||
|
||||
<p>He has spent, at the current time, about 2 years working on this blog. Most of his posts are about all of the work it took and everything he learned making this blog. Unlike most "bloggers", he has started with many blog posts and no blog, rather than a blog without posts. "Someday", he says, "I will actually get that blog up". I always nod encouragingly.</p>
|
||||
|
||||
<p>If you are reading this, then that day has arrived. We hope you enjoy it, and maybe even learn something along the way.</p>
|
||||
</article>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
article {
|
||||
max-width: var(--content-width);
|
||||
margin: 0 auto;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,14 @@
|
||||
---
|
||||
import { listPosts } from '@lib/content.ts';
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import Post from '@components/Post.astro';
|
||||
|
||||
const entries = await listPosts();
|
||||
// there will always be at leaste one entry
|
||||
const entry = entries[0]!;
|
||||
const prevSlug = entries[1] ? entries[1]?.id : null;
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<p>Index file</p>
|
||||
<BaseLayout pageTitle={entry.data.title}>
|
||||
<Post {entry} {prevSlug} />
|
||||
</BaseLayout>
|
||||
|
||||
80
src/pages/posts.astro
Normal file
80
src/pages/posts.astro
Normal file
@@ -0,0 +1,80 @@
|
||||
---
|
||||
import '@styles/prose.css';
|
||||
|
||||
import { render } from 'astro:content';
|
||||
import { listPosts } from '@lib/content.ts';
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
|
||||
// return all posts in dev, only non-draft in prod
|
||||
const entries = await listPosts();
|
||||
const posts = await Promise.all(entries.map(async entry => {
|
||||
const { remarkPluginFrontmatter } = await render(entry);
|
||||
return { ...entry, remarkPluginFrontmatter };
|
||||
}));
|
||||
---
|
||||
|
||||
<BaseLayout pageTitle="Posts">
|
||||
<div class="prose">
|
||||
<h1>All Posts</h1>
|
||||
<ul>
|
||||
{posts.map( (p, idx) =>
|
||||
<li>
|
||||
{idx > 0 && <hr></hr>}
|
||||
<h2>
|
||||
<a href={`/${p.id}`} data-astro-prefetch>
|
||||
{p.data.title}
|
||||
</a>
|
||||
{p.data.draft && <span class="draft-notice">Draft</span>}
|
||||
</h2>
|
||||
<p>
|
||||
{p.remarkPluginFrontmatter.description}
|
||||
</p>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
<style>
|
||||
div.prose {
|
||||
max-width: var(--content-width);
|
||||
padding: var(--content-padding);
|
||||
margin: 0 auto;
|
||||
|
||||
& h2 a {
|
||||
color: var(--content-color);
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
h2 {
|
||||
display: flex;
|
||||
gap: 01rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.draft-notice {
|
||||
font-size: 0.5em;
|
||||
font-family: 'Figtree Variable', sans-serif;
|
||||
color: var(--nav-link-color);
|
||||
background-color: var(--accent-color);
|
||||
padding: 0.05em 0.5em;
|
||||
border-radius: 20% / 50%;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
height: 1px;
|
||||
margin: 2.5rem 0;
|
||||
background-color: var(--content-color);
|
||||
}
|
||||
</style>
|
||||
33
src/plugins.ts
Normal file
33
src/plugins.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { Root, Paragraph, PhrasingContent } from 'mdast';
|
||||
import type { VFile } from 'vfile';
|
||||
import { visit } from 'unist-util-visit';
|
||||
import { toString } from 'mdast-util-to-string';
|
||||
|
||||
|
||||
export function remarkDescription() {
|
||||
return (tree: Root, vfile: VFile) => {
|
||||
let description: string | null = null;
|
||||
|
||||
visit(tree, 'paragraph', (node: Paragraph) => {
|
||||
if (description !== null) return;
|
||||
description = summarize(node);
|
||||
});
|
||||
|
||||
// Astro exposes this as `remarkPluginFrontmatter` from render()
|
||||
const astro = (vfile.data.astro ??= { frontmatter: {} }) as { frontmatter: Record<string, unknown> };
|
||||
astro.frontmatter.description = description;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convert a paragraph node to a plain-text string, stripping any
|
||||
* MDX JSX elements (e.g. <Sidenote>) so their content doesn't
|
||||
* leak into the summary.
|
||||
*/
|
||||
function summarize(par: Paragraph): string {
|
||||
const filtered = par.children.filter(
|
||||
(child: PhrasingContent) => !(child.type as string).startsWith('mdxJsx')
|
||||
);
|
||||
return toString({ type: 'paragraph', children: filtered });
|
||||
}
|
||||
Reference in New Issue
Block a user