Added Sidenote Component

This commit is contained in:
2025-10-02 14:18:46 +02:00
parent 0111cd71fe
commit 054d450273
78 changed files with 1218 additions and 524 deletions

71
content/meta/index.mdoc Normal file
View File

@@ -0,0 +1,71 @@
---
title: A shape of things to come
cover:
src: /images/covers/meta/index/cover/src.jpeg
alt: Pile of Skulls
caption: >-
To crush your players, see them driven before you, and to hear their
wailings of their characters
showInHeader: true
meta:
publicationDate: 2025-10-01T10:30:00.000Z
status: draft
isFeatured: true
tags:
- how-and-what
author: dave-damage
seo:
noIndex: false
---
Vester cælestis pascit illa{% Sidenote #Test marker="⋄" content="In partes Galilææ. Et veniens habitavit in civitate quæ vocatur Nazareth ut adimpleretur quod dictum est per prophetas Quoniam Nazaræus vocabitur" type="default" /%}. Nonne vos magis pluris estis illis? Quis autem vestrum cogitans potest adjicere ad staturam suam cubitum unum? Et de vestimento quid solliciti estis? Considerate lilia agri quomodo crescunt non laborant, neque nent. Dico autem vobis, quoniam nec Salomon in omni gloria sua coopertus est sicut unum ex istis. Si autem fœnum agri, quod hodie est, et cras in clibanum mittitur, Deus sic vestit, quanto magis vos modicæ fidei? Nolite ergo solliciti esse, dicentes Quid manducabimus, aut quid bibemus, aut quo operiemur? hæc enim omnia gentes inquirunt. Scit enim Pater vester, quia his omnibus indigetis. Quærite.
{% table %}
- Ubi
- Stell
---
- 3986
- 7176
---
- 7673
- 3875
{% /table %}
## Videantur ab hominibus
In partes Galilææ. Et veniens habitavit in civitate quæ vocatur Nazareth ut adimpleretur quod dictum est per prophetas Quoniam Nazaræus vocabitur. In diebus autem illis venit Joannes **multorum** Baptista prædicans in deserto Judææ, et dicens Pœnitentiam agite appropinquavit enim regnum cælorum. Hic est enim, qui dictus est per Isaiam prophetam dicentem Vox clamantis in deserto Parate viam Domini; rectas facite semitas ejus. Ipse autem Joannes habebat vestimentum **fiant qui** de pilis camelorum, et zonam pelliceam circa lumbos suos esca autem ejus erat locustæ, et mel silvestre. Tunc exibat ad eum Jerosolyma, et omnis Judæa.
### Venti et mare obediunt
- Discipulis ejus ait illi domine permitte.
- Inducas in tentationem sed libera nos a **pallium** malo.
- Recumbent cum abraham et isaac et
- Meum jesus autem ait illi sequere *omnis enim that small* me et.
- Sua nolite judicare ut not judicemini in
- Fidei tunc *altare et* **autem sermo vester** surgens imperavit.
- Es mitte te *infirmitatem* deorsum scriptum.
- Judorum vidimus enim stellam *dico vobis quia* ejus.
#### Procedit de ore dei
1. De vestimento **sua nolite judicare ut** quid solliciti **quoniam ipsi consolabuntur** estis considerate lilia.
1. Animam pueri qui consurgens accepit puerum et.
1. Dicens beati pauperes spiritu quoniam
1. Vobis quoniam nec salomon in.
1. Estis sal terr quod si *eleemosynam nesciat sinistra* sal evanuerit.
1. Jesus manum tetigit eum dicens volo [Ab eo](https://example.com/erant/fructum) *arcta is qu ducit* **in manu** mundare.
1. Dimisit eam febris et surrexit et ministrabat *dei* eis.
1. Tuus qui ==sic omnis== *per isaiam* videt in.
##### Which dificavit domum suam
Dico vobis quia omnis qui viderit mulierem ad concupiscendum eam, jam mœchatus est eam in corde suo. Quod si oculus tuus dexter scandalizat te, erue eum, et projice abs te expedit enim tibi ut pereat unum membrorum tuorum, quam totum corpus tuum mittatur in gehennam. Et si dextra manus tua scandalizat te, abscide eam, et projice abs te expedit enim tibi ut pereat unum membrorum tuorum, quam totum corpus tuum eat in gehennam. Dictum est autem Quicumque dimiserit uxorem suam, det ei libellum repudii. Ego autem dico vobis quia omnis.
###### Malo est audistis
Ipsi Deum videbunt. Beati pacifici quoniam filii Dei vocabuntur. Beati qui persecutionem patiuntur propter justitiam quoniam ipsorum est regnum cælorum. Beati estis cum maledixerint vobis, et persecuti vos fuerint, et dixerint omne malum adversum vos mentientes, propter me gaudete, et exsultate, quoniam merces vestra copiosa est in cælis. Sic enim persecuti sunt prophetas, qui fuerunt ante vos. Vos estis sal terræ. Quod si sal evanuerit, in quo salietur? ad nihilum valet ultra, nisi ut mittatur foras, et conculcetur ab hominibus. Vos estis lux mundi. Non potest civitas abscondi.
Prophetam dicentem dicens Surge, et accipe puerum, et *qui dmonia habebant et* matrem ejus, et vade in terram Israël defuncti sunt enim qui quærebant animam pueri. Qui consurgens, accepit puerum, et matrem **grotationes** ejus, et venit in terram Israël. Audiens autem quod Archelaus regnaret in Judæa **reddes autem domino juramenta** pro Herode patre suo, timuit illo ire et admonitus in somnis, secessit in partes Galilææ. Et veniens habitavit in civitate quæ vocatur Nazareth ut adimpleretur quod dictum est *in* per prophetas Quoniam Nazaræus vocabitur. In.
Videns autem multos pharisæorum{% Sidenote #non-unique marker="*" content="In partes Galilææ. Et veniens habitavit in civitate quæ vocatur Nazareth ut adimpleretur quod dictum est per prophetas Quoniam Nazaræus vocabitur. In diebus autem illis venit Joannes multorum Baptista prædicans in deserto Judææ, et dicens Pœnitentiam agite appropinquavit enim regnum cælorum. Hic est enim, qui dictus est per Isaiam prophetam dicentem Vox clamantis in deserto Parate viam Domini; rectas facite semitas ejus. Ipse autem Joannes habebat vestimentum fiant qui de pilis camelorum, et zonam pelliceam circa lumbos suos esca autem ejus erat locustæ, et mel silvestre. Tunc exibat ad eum Jerosolyma, et omnis Judæa." type="default" /%}, et sadducæorum, venientes ad baptismum suum, dixit eis Progenies viperarum, quis demonstravit vobis fugere a ventura ira? Facite ergo fructum dignum pœnitentiæ. Et ne velitis dicere intra vos Patrem habemus Abraham. Dico enim vobis quoniam potens est Deus de lapidibus istis suscitare filios Abrahæ. Jam enim securis ad radicem arborum posita est. Omnis ergo arbor, quæ non facit fructum bonum, excidetur, et in ignem mittetur. Ego **flumina** quidem baptizo vos **mentientes propter** in aqua in.
Sal terræ. Quod si sal evanuerit, in quo salietur? ad nihilum valet ultra, nisi ut mittatur foras, et conculcetur ab hominibus. Vos estis lux mundi. Non potest civitas abscondi supra montem posita, neque accendunt lucernam, et ponunt eam sub modio, sed super candelabrum, ut luceat omnibus qui in domo sunt. Sic luceat lux vestra coram hominibus ut videant opera vestra bona, **admirabantur turb super doctrina** et glorificent Patrem vestrum, qui in cælis est. Nolite putare quoniam veni solvere legem aut prophetas non veni solvere, sed adimplere. Amen quippe dico vobis, donec transeat cælum et terra, jota unum aut unus apex non præteribit **jussit** a.

View File

@@ -14,7 +14,7 @@
},
{
"name": "Kitchensink",
"path": "/",
"path": "/kitchensink",
"gridPosition": "area_2",
"variant": "kitchensink",
"sublinks": {
@@ -123,17 +123,17 @@
"path": "/chainbreaker",
"gridPosition": "area_5",
"variant": "chainbreaker",
"background": "/images/categories/items/4/background.png",
"background": "/images/categories/items/4/background.jpg",
"sublinks": {
"discriminant": false
},
"subtitle": {
"discriminant": true,
"value": {
"content": "Spear & Animism",
"content": "A history of Violence",
"divider": {
"discriminant": true,
"value": ""
"value": ""
}
}
}

View File

@@ -0,0 +1,15 @@
{
"name": "Dave Damage",
"avatar": "/images/authors/dave-damage/avatar.png",
"description": "Nothing nice 2 say",
"contacts": [
{
"type": "email",
"url": "ottonom@gmail.com"
},
{
"type": "discord",
"url": "381079209197699083"
}
]
}

View File

@@ -0,0 +1,8 @@
{
"name": "Was ist Was?",
"icon": {
"discriminant": "glyph",
"value": "‽"
},
"description": "General information and introductions for stuff"
}

View File

@@ -1,11 +1,18 @@
import { config } from '@keystatic/core';
import navigation from '@/keystatic/singletons/navigation';
import NavSingleton from '@/keystatic/singletons/navigation';
import AuthorsCollection from '@/keystatic/collections/taxonomy/authors';
import TagsCollection from '@/keystatic/collections/taxonomy/tags';
import MetaPostsCollection from '@/keystatic/collections/meta/article';
export default config({
storage: {
kind: 'local',
},
collections: {},
singletons: { navigation },
singletons: { navigation: NavSingleton },
collections: {
authors: AuthorsCollection,
tags: TagsCollection,
meta_posts: MetaPostsCollection,
},
});

View File

@@ -10,6 +10,7 @@
"format": "prettier --write ."
},
"dependencies": {
"@keystar/ui": "^0.7.19",
"@keystatic/core": "^0.5.48",
"@keystatic/next": "^5.0.4",
"@markdoc/markdoc": "^0.5.4",

3
pnpm-lock.yaml generated
View File

@@ -8,6 +8,9 @@ importers:
.:
dependencies:
'@keystar/ui':
specifier: ^0.7.19
version: 0.7.19(next@15.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@keystatic/core':
specifier: ^0.5.48
version: 0.5.48(next@15.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-dom@19.1.0(react@19.1.0))(react@19.1.0)

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 KiB

BIN
public/images/swamp.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -9,7 +9,7 @@ export default function SiteLayout({
<MenuProvider>
<PageHeader />
<PageMenu />
<main>{children}</main>
{children}
</MenuProvider>
);
}

View File

@@ -0,0 +1,29 @@
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { Author, Tag } from '@/lib/types/taxonomy';
import Article from '@/components/Article';
import { getMetaHome } from '@/lib/readers/meta/posts';
import generatePageMetaData from '@/lib/next/generatePageMetaData';
import { getAuthorBySlug } from '@/lib/readers/taxonomy/authors';
import { getTagBySlug } from '@/lib/readers/taxonomy/tags';
export async function generateMetadata(): Promise<Metadata> {
const article = await getMetaHome();
if (!article) return { title: 'Not Found' };
return generatePageMetaData(article);
}
export default async function MetaHome() {
const article = await getMetaHome();
if (!article) notFound();
const author = await getAuthorBySlug(article.meta.author);
const tags = await Promise.all(
article.meta.tags.map((slug: string) => getTagBySlug(slug))
);
return <Article article={article} tags={tags} author={author} />;
}

View File

@@ -6,488 +6,6 @@ export default function Home() {
<h1>
DAVE! DAVE! Do Not Let Us Die In The Dark Night Of This Cold Winter!
</h1>
<h2>Background / Image effects</h2>
<div className={styles.grid}>
<div className={`${styles.demoCard}`}>
<h3 className={styles.demoTitle}>Overexposure Blast</h3>
<div className={styles.imageContainer}>
<img
className={`${styles.demoImage} anim-overexposure`}
src="https://images.pexels.com/photos/1096925/pexels-photo-1096925.jpeg"
alt="Demo"
/>
</div>
</div>
<div className={`${styles.demoCard}`}>
<h3 className={styles.demoTitle}>Contrast Slam</h3>
<div className={styles.imageContainer}>
<img
className={`${styles.demoImage} anim-contrastslam`}
src="https://images.pexels.com/photos/119809/pexels-photo-119809.jpeg"
alt="Demo"
/>
</div>
</div>
<div className={`${styles.demoCard}`}>
<h3 className={styles.demoTitle}>Colorbleed</h3>
<div className={styles.imageContainer}>
<img
className={`${styles.demoImage} anim-colorbleed`}
src="https://images.pexels.com/photos/12924931/pexels-photo-12924931.jpeg"
alt="Demo"
/>
</div>
</div>
<div className={`${styles.demoCard}`}>
<h3 className={styles.demoTitle}>Film Burn</h3>
<div className={styles.imageContainer}>
<img
className={`${styles.demoImage} anim-filmburn`}
src="https://images.pexels.com/photos/1031357/pexels-photo-1031357.jpeg"
alt="Demo"
/>
</div>
</div>
<div className={`${styles.demoCard}`}>
<h3 className={styles.demoTitle}>Photocopier Malfunction</h3>
<div className={styles.imageContainer}>
<img
className={`${styles.demoImage} anim-malfunction`}
src="https://images.pexels.com/photos/3054252/pexels-photo-3054252.jpeg"
alt="Demo"
/>
</div>
</div>
<div className={`${styles.demoCard}`}>
<h3 className={styles.demoTitle}>Toner Starvation</h3>
<div className={styles.imageContainer}>
<img
className={`${styles.demoImage} anim-tonerstarvation`}
src="https://images.pexels.com/photos/3822728/pexels-photo-3822728.jpeg"
alt="Demo"
/>
</div>
</div>
<div className={`${styles.demoCard}`}>
<h3 className={styles.demoTitle}>Digital Corruption</h3>
<div className={styles.imageContainer}>
<img
className={`${styles.demoImage} anim-corruption`}
src="https://images.pexels.com/photos/220793/pexels-photo-220793.jpeg"
alt="Demo"
/>
</div>
</div>
<div className={`${styles.demoCard}`}>
<h3 className={styles.demoTitle}>Stark Flash</h3>
<div className={styles.imageContainer}>
<img
className={`${styles.demoImage} anim-starkflash`}
src="https://images.pexels.com/photos/13744675/pexels-photo-13744675.jpeg"
alt="Demo"
/>
</div>
</div>
</div>
<div className={styles.instructions}>
<h2>Instructions</h2>
<p>
Hover over each image to see the effect. These are designed to work
with your base filter:
</p>
<code>filter: grayscale(1) contrast(150%) brightness(120%)</code>
<p>
You can replace the demo images with your own by changing the src
attributes. All effects use stepped animations or sharp transitions to
maintain that industrial, non-digital feel.
</p>
<p>
The effects range from subtle (Contrast Slam) to more dramatic
(Digital Corruption). Choose based on how aggressive you want the
interaction to feel.
</p>
</div>
<h2>Link Effects</h2>
<div className={styles.grid}>
{/* Your Ideas */}
<div className={styles.demoCard}>
<h3 className={styles.demoTitle}>Strikethrough Mark (Marker)</h3>
<p>
This is some text with a{' '}
<a href="#" className="anim-strikethroughmarker">
strikethrough link
</a>{' '}
in the middle of it.
</p>
</div>
<div className={styles.demoCard}>
<h3 className={styles.demoTitle}>Strikethrough Mark (Industrial)</h3>
<p>
This is some text with a{' '}
<a href="#" className="anim-strikethroughindustrial">
strikethrough link
</a>{' '}
in the middle of it.
</p>
</div>
<div className={styles.demoCard}>
<h3 className={styles.demoTitle}>Marker Highlight [Industrial]</h3>
<p>
This is some text with a{' '}
<a href="#" className="anim-markerhighlightindustrial">
marker highlight link
</a>{' '}
in the middle of it.
</p>
</div>
{/* Stamping/Punching Effects */}
<div className={styles.demoCard}>
<h3 className={styles.demoTitle}>Label Maker</h3>
<p>
This is some text with a{' '}
<a href="#" className="anim-labelmaker">
label maker link
</a>{' '}
in the middle of it.
</p>
</div>
<div className={styles.demoCard}>
<h3 className={styles.demoTitle}>Rubber Stamp</h3>
<p>
This is some text with a{' '}
<a href="#" className="anim-rubberstamp">
rubber stamp link
</a>{' '}
in the middle of it.
</p>
</div>
{/* Industrial/Mechanical */}
<div className={styles.demoCard}>
<h3 className={styles.demoTitle}>Press/Stamp</h3>
<p>
This is some text with a{' '}
<a href="#" className="anim-pressstamp">
pressed link
</a>{' '}
in the middle of it.
</p>
</div>
<div className={styles.demoCard}>
<h3 className={styles.demoTitle}>Typewriter Underline</h3>
<p>
This is some text with a{' '}
<a href="#" className="anim-typewriter">
typewriter link
</a>{' '}
in the middle of it.
</p>
</div>
<div className={styles.demoCard}>
<h3 className={styles.demoTitle}>Hard Invert</h3>
<p>
This is some text with a{' '}
<a href="#" className="anim-hardInvert">
hard invert link
</a>{' '}
in the middle of it.
</p>
</div>
{/* Marking/Annotation */}
<div className={styles.demoCard}>
<h3 className={styles.demoTitle}>Bracket Annotation</h3>
<p>
This is some text with a{' '}
<a href="#" className="anim-pointer">
bracket link
</a>{' '}
in the middle of it.
</p>
</div>
<div className={styles.demoCard}>
<h3 className={styles.demoTitle}>Corner Box</h3>
<p>
This is some text with a{' '}
<a href="#" className="anim-cornerbox">
corner box link
</a>{' '}
in the middle of it.
</p>
</div>
{/* Glitch/Digital */}
<div className={styles.demoCard}>
<h3 className={styles.demoTitle}>Character Glitch</h3>
<p>
This is some text with a{' '}
<a href="#" className="anim-characterglitch">
glitch link
</a>{' '}
in the middle of it.
</p>
</div>
<div className={styles.demoCard}>
<h3 className={styles.demoTitle}>Pixel Shift</h3>
<p>
This is some text with a{' '}
<a href="#" className="anim-pixelshift">
shifting link
</a>{' '}
in the middle of it.
</p>
</div>
{/* Extra Ideas */}
<div className={styles.demoCard}>
<h3 className={styles.demoTitle}>Redacted/Censored</h3>
<p>
This is some text with a{' '}
<a href="#" className="anim-redacted">
redacted link
</a>{' '}
in the middle of it.
</p>
</div>
<div className={styles.demoCard}>
<h3 className={styles.demoTitle}>X-Ray/Negative</h3>
<p>
This is some text with an{' '}
<a href="#" className="anim-xray">
x-ray link
</a>{' '}
in the middle of it.
</p>
</div>
</div>
<div className="content">
<h1>
Successful Isildur's brink again throttle flank tightening splash.
</h1>
<p>
Help illusion embrace liquor tightening intelligence Maggot's whips
bit forests. 17 sing impassable helps Southrons beheading. What's the
Elvish word for 'friend'?
</p>
<h2>Give Hobbitses lend yours lads picking uniting sometime.</h2>
<p>
Inferno shaken skin undo wars close circles verse suck Dwarves. I gave
you the chance of aiding me willingly, but you have elected the way of
pain! Precautions tower tied Rivendell everyone agents wouldn't?
</p>
<h3>Doorway Mithrandir clearing wielder strengths floor?</h3>
<p>
Pillaged pointy-eared mix charm Grond confounded able-bodied tact
glimpse instruction open dear. Suffering powerful capable gulls
famousest stroke breathes Bilbo squeaked pace chances. Let the
Ring-bearer decide.
</p>
<ul>
<li>Mirkwood.</li>
<li>Tom.</li>
<li>Bilbo's.</li>
<li>Gandalf's.</li>
<li>Dwarvish.</li>
</ul>
<ul>
<li>Queen wants Oin loose heads decay piety!</li>
<li>Large happening arrived owes legends wit war bled Durin's.</li>
<li>
Pursuit exactly during relief mission meats cause Noldorin ablaze
tracked.
</li>
<li>Darken knife midday meat Goblinses.</li>
<li>
Your Rabble-rousers greatest could beast thirty-four t wizards
slumbers reforge.
</li>
</ul>
<h3>Late vagabond knowing Ent legends there flattened cultured?</h3>
<p>
Swords are no more use here. Deny Chubbs restored. Scare rebuild
Argonath tracked day's large.
</p>
<ol>
<li>Easterlings!</li>
<li>Bill.</li>
<li>Dúnedain!</li>
<li>Gandalf.</li>
<li>Orcrist.</li>
</ol>
<ol>
<li>Answers feelings Elrond conjurer runs.</li>
<li>Sul nudge powerless jelly dumping hair grows log forgave?</li>
<li>
Retaken succumbed funeral courtyard Glóin incident mere somewhere
commander assistance?
</li>
<li>Bore outrun stead fight Athelas guardroom willing contains.</li>
<li>Hunted Angmar wager noose arguing?</li>
</ol>
<h3>Elros next own wisp whence cakehole right!</h3>
<p>
Thank hid its lessened lined tells prefers were Stone-Giants thousand
troubles. Earendil staff pines bog finest mushroom consumption.
Mistaken streaming fates paths arts puppet Barad-dûr uniting? You
shall not pass!
</p>
<table>
<thead>
<tr>
<th></th>
<th>Ales</th>
<th>Deeply</th>
<th>Mortality</th>
<th>Open</th>
<th>Single-handedly</th>
</tr>
</thead>
<tbody>
<tr>
<td>Band</td>
<td>appeared</td>
<td>waited</td>
<td>whose</td>
<td>plate</td>
<td>marshaling</td>
</tr>
<tr>
<td>Meat</td>
<td>nest</td>
<td>thatched</td>
<td>rallying</td>
<td>claimed</td>
<td>hungers</td>
</tr>
<tr>
<td>Mouse</td>
<td>also</td>
<td>birdses</td>
<td>moons</td>
<td>strain</td>
<td>brightest</td>
</tr>
<tr>
<td>Hate</td>
<td>different</td>
<td>arrangements</td>
<td>chiefest</td>
<td>think</td>
<td>try</td>
</tr>
<tr>
<td>Something's</td>
<td>task</td>
<td>here's</td>
<td>decent</td>
<td>someone</td>
<td>uses</td>
</tr>
<tr>
<td>Shirt</td>
<td>tonight</td>
<td>bay</td>
<td>beautifully</td>
<td>tad</td>
<td>cloaks</td>
</tr>
</tbody>
<tfoot>
<tr>
<td>revenge</td>
<td>teaching</td>
<td>mischief</td>
<td>shores</td>
<td>dreams</td>
<td>tested</td>
</tr>
</tfoot>
</table>
<h4>Stage dishcloth 20 horses Tooks souls crawl crime.</h4>
<p>
Approve Sul wilderness grave embellishment greatly over these rack
struggle. End ringing bell Anor halls pairs thirst fortress curtain
cleared? Dwelt language 1296 Underhill nasty. I gave you the chance of
aiding me willingly, but you have elected the way of pain!
</p>
<blockquote>
<p>You are full of surprises, Master Baggins.</p>
<footer>
Girion, <cite>warmongering appears</cite>
</footer>
</blockquote>
<h5>
Sung outscoring fingers Fundin reaction inquiries buggers deadliest.
</h5>
<p>
Mustn't powerless pierces Muil sorry crossing diamond brandy. Rip
rockets hinder Braga go had web ought sakes hail. A wizard is never
late, Frodo Baggins. Nor is he early. He arrives precisely when he
means to. Ashes tie Gamgee dicky!
</p>
<pre>
Rhudaur fancy tilled heart beggars. Dwarf nothing talked foot club.
Slaughtered flatten Hobbit journey's four-day?
</pre>
<h6>Pearl tact tomb bits Arwen Evenstar worry?</h6>
<p>
Times unspoiled <time>defenses</time> Silvan.{' '}
<del>Sigrid Pippin Gandalf</del> thin stubbornness noises easily
spread. Eldar warriors <code>won answered</code> filth yourself
pocket. Showing store consistency <kbd>M</kbd> crevice. Decision
feverfew <sup>giving</sup> Misty Mountain lord supplant. Gorgoroth{' '}
<dfn>load born fulfilled plenty</dfn> fates serpent. Doorstep Pippin's{' '}
<cite>pity bridge</cite> long weak weep? Brightest <abbr>Chubbs</abbr>{' '}
jewels understand. Somewhat Erebor <sub>noise</sub> squealing moved?
Pippin's <mark>feels overrun</mark> hours brown burns. <var>Anor</var>{' '}
turning pick prophecy. Surrounded <q>entered needlessly weary</q> vile
hmm Bagshot Row. Consent <ins>outwitted dotage slug</ins> Homely hear.
Parapet <strong>protected favored defied roam</strong> quiet Dori sick
bent. <samp>Homage store hurricane</samp> prove ferret Helm's Deep
lately? <small>Excellent regret fun often</small> returned Wood-elves
apocalypse. Théodred's rights rat drawing{' '}
<a>examine dared bygone residence deeply</a>.{' '}
<em>Greenway Girion Rohirrim</em> trammel waiting edge.
</p>
<hr />
<dl>
<dt>Turn</dt>
<dd>
Bare protuberance arrived forging funny salvage Cair except first
banners.
</dd>
<dt>Foes</dt>
<dd>Bore river large house shadows it's Tuckborough warn.</dd>
<dd>
Stirring Greenwood nest sapphire grant gob flagon famous mean!
</dd>
<dt>Unprepared</dt>
<dd>
Single-handed wriggling creatures lock canopy anytime horses defense
Hobbit's?
</dd>
<dd>
Nûmenor dungeons achieving encourage fretting dines believes
understand.
</dd>
<dd>Ease love shine legs wee harbor Udùn adventure tumble stays.</dd>
</dl>
</div>
</div>
);
}

View File

@@ -0,0 +1,3 @@
.article {
@mixin layout-wrapper;
}

View File

@@ -0,0 +1,23 @@
.wrapper {
@mixin mx auto;
@mixin px var(--spacing-comfortable);
max-width: 90ch;
font-size: clamp(1rem, 2.5vw, 1.5rem);
}
.hasMargin {
@media screen and (--bp-margin) {
@mixin px 0;
display: flex;
gap: var(--spacing-generous);
max-width: none;
& .content {
flex: 1 1 auto;
min-width: 50ch;
max-width: 75ch;
}
}
}

View File

@@ -0,0 +1,56 @@
import React from 'react';
import Markdoc from '@markdoc/markdoc';
import type { Node } from '@markdoc/markdoc';
import styles from './MarkdocRenderer.module.css';
import { nodes } from '@/lib/markdoc/nodes';
import { tags } from '@/lib/markdoc/tags';
import { collectSideNotes, hasComponents } from '@/lib/markdoc/utils';
import { Column } from '@/components/Content/Grid/Column';
import { Row } from '@/components/Content/Grid/Row';
import Sidenote from '@/components/Content/Sidenote/Item';
import SidenoteContainer from '@/components/Content/Sidenote/Container';
interface MarkdocRendererProps {
content: () => Promise<{ node: Node }>;
className?: string;
}
const MARGIN_COMPONENTS = ['Sidenote'];
export default async function MarkdocRenderer({
content,
className,
}: MarkdocRendererProps) {
const components = {
Column,
Row,
Sidenote,
};
const { node } = await content();
const errors = Markdoc.validate(node, { tags, nodes });
if (errors.length) {
console.error('Markdoc validation errors:', errors);
throw new Error('Invalid Content');
}
const renderable = Markdoc.transform(node, { tags, nodes });
const sidenotes = collectSideNotes(renderable);
const showMargin = hasComponents(renderable, MARGIN_COMPONENTS);
return (
<div
className={`${styles.wrapper} ${className} ${showMargin ? styles.hasMargin : ''}`}
>
<div className={`${styles.content} content`}>
{Markdoc.renderers.react(renderable, React, { components })}
</div>
<SidenoteContainer items={sidenotes} />
</div>
);
}

View File

@@ -0,0 +1,10 @@
import MarkdocRenderer from '@/components/Article/Content/MarkdocRenderer';
import { ArticleContent } from '@/lib/types/content';
interface ContentProps {
article: ArticleContent;
}
export default function Content({ article }: ContentProps) {
return <MarkdocRenderer content={article.content} />;
}

View File

View File

@@ -0,0 +1,36 @@
import Image from 'next/image';
import styles from './Cover.module.css';
interface CoverProps {
cover: {
readonly src: string;
readonly alt: string;
readonly caption: string;
readonly showInHeader: boolean;
};
}
export default function Cover({ cover }: CoverProps) {
return (
<figure className={styles.cover}>
<Image
src={cover.src}
alt={cover.alt || 'Standard Alt'}
width={0}
height={0}
sizes={'100vw'}
className={styles.image}
/>
<figcaption className={styles.caption}>
<div className={styles.captionwrapper}>
{cover.caption ? (
<span className={styles.captiontext}>{cover.caption}</span>
) : (
<span className={styles.covermeta}>{cover.src}</span>
)}
</div>
</figcaption>
</figure>
);
}

View File

@@ -0,0 +1,60 @@
import Link from 'next/link';
import { Author, Tag } from '@/lib/types/taxonomy';
import { toMilitaryDTG } from '@/lib/utils/date';
import styles from './Meta.module.css';
interface MetaProps {
author: Author | null;
tags: Tag[];
updateDate?: string;
publicationDate: string;
}
export default function Meta({
author,
tags,
updateDate,
publicationDate,
}: MetaProps) {
return (
<div className={styles.meta}>
{author && (
<div className={styles.section}>
<span className={styles.label}>Author</span>
<span className={styles.author}>
<Link href={author.slug}>{author.name}</Link>
</span>
</div>
)}
{tags && tags.length > 0 && (
<div className={styles.section}>
<span className={styles.label}>Tags</span>
<span className={styles.author}>
<ul className={styles.taglist}>
{tags.map((tag) => (
<li key={tag.slug} className={styles.tag}>
<Link className={styles.link} href={`/tags/${tag.slug}`}>
{tag.name}
</Link>
</li>
))}
</ul>
</span>
</div>
)}
{publicationDate && (
<div className={styles.metasection}>
<span className={styles.metalabel}>Last Update</span>
<time className={styles.updatedate}>
{updateDate
? toMilitaryDTG(updateDate)
: toMilitaryDTG(publicationDate)}
</time>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,46 @@
import Link from 'next/link';
import styles from './Overline.module.css';
import { toMilitaryDTG } from '@/lib/utils/date';
interface OverlineProps {
breadcrumbs?: string[];
publicationDate: string;
}
export default function Overline({
breadcrumbs = [],
publicationDate,
}: OverlineProps) {
const breadcrumber = breadcrumbs.map((crumb, index) => {
const isLast = index === breadcrumbs.length - 1;
const href = `/${breadcrumbs.slice(0, index + 1).join('/')}`;
return (
<li key={index} className={styles.crumb}>
<span className={styles.separator}>/</span>
{isLast ? (
<span className={styles.current}>{crumb}</span>
) : (
<Link href={href} className={styles.link}>
{crumb}
</Link>
)}
</li>
);
});
return (
<div className={styles.overline}>
<ul className={styles.breadcrumbs}>
<li className={styles.crumb}>
<Link href="/" className={styles.link}>
dave-dmg.de
</Link>
</li>
{breadcrumber}
</ul>
<time className={styles.publicationDate}>
{toMilitaryDTG(publicationDate)}
</time>
</div>
);
}

View File

@@ -0,0 +1,14 @@
import styles from './Title.module.css';
import { JSX } from 'react';
interface TitleProps {
title: string;
}
export default function Title({ title }: TitleProps) {
return (
<div className={styles.wrapper}>
<h1 className={styles.title}>{title}</h1>
</div>
);
}

View File

@@ -0,0 +1,41 @@
import { ArticleContent } from '@/lib/types/content';
import { Author, Tag } from '@/lib/types/taxonomy';
import { isValidCover } from '@/lib/utils/guards';
import styles from './Header.module.css';
import Overline from '@/components/Article/Header/Overline';
import Title from '@/components/Article/Header/Title';
import Meta from '@/components/Article/Header/Meta';
import Cover from '@/components/Article/Header/Cover';
interface HeaderProps {
article: ArticleContent;
breadcrumbs?: string[];
author: Author | null;
tags: Tag[];
}
export default function Header({
article,
breadcrumbs,
author,
tags,
}: HeaderProps) {
const { title, meta, cover } = article;
return (
<header className={styles.container}>
<Overline
publicationDate={article.meta.publicationDate}
breadcrumbs={breadcrumbs}
/>
<Title title={article.title} />
{cover && isValidCover(cover) && <Cover cover={cover} />}
<Meta
tags={tags}
author={author}
updateDate={meta.updateDate}
publicationDate={meta.publicationDate}
/>
</header>
);
}

View File

@@ -0,0 +1,32 @@
import type { ArticleContent } from '@/lib/types/content';
import { Author, Tag } from '@/lib/types/taxonomy';
import Header from './Header';
import Content from '@/components/Article/Content';
import styles from './Article.module.css';
interface ArticleProps {
article: ArticleContent;
author: Author | null;
tags: Tag[];
breadcrumbs?: string[];
}
export default function Article({
article,
author,
tags,
breadcrumbs,
}: ArticleProps) {
return (
<article className={styles.article}>
<Header
article={article}
author={author}
tags={tags}
breadcrumbs={breadcrumbs}
/>
<Content article={article} />
</article>
);
}

View File

@@ -0,0 +1,3 @@
.column {
position: relative;
}

View File

@@ -0,0 +1,23 @@
import React from 'react';
import styles from './Column.module.css';
interface ColumnProps {
style: {
colspan: number;
};
children?: React.ReactNode;
}
export function Column({ style, children }: ColumnProps) {
return (
<div
className={styles.column}
style={{
gridColumn: `span ${style.colspan}`,
}}
>
{children}
</div>
);
}

View File

@@ -0,0 +1,3 @@
.row {
display: grid;
}

View File

@@ -0,0 +1,29 @@
import React from 'react';
import styles from './Row.module.css';
interface RowProps {
style: {
cols: number;
gap: string;
align: string;
justify: string;
};
children: React.ReactNode;
}
export function Row({ style, children }: RowProps) {
return (
<div
className={`${styles.row}`}
style={{
gridTemplateColumns: `repeat(${style.cols}, 1 fr)`,
gap: `${style.gap}`,
alignItems: `${style.align}`,
justifyContent: `${style.justify}`,
}}
>
{children}
</div>
);
}

View File

@@ -0,0 +1,63 @@
.container {
flex: 1 0 auto;
min-width: 16rem;
}
.wrapper {
@media screen and (--bp-margin) {
position: absolute;
top: anchor(top);
}
}
.note {
position: relative;
min-width: 16rem;
font-family: var(--font-mono);
@media screen and (--bp-margin) {
@mixin text-xs;
margin-right: var(--spacing-generous);
padding: var(--spacing-cozy) var(--spacing-cozy) var(--spacing-cozy) var(--size-12) ;
border: var(--size-1) solid var(--color-surface-inverse);
background: var(--color-surface-elevated-1);
box-shadow: 2px 2px 0 var(--color-palette-woodsmoke), 4px 4px 0 var(--color-palette-charcoal-gray);
}
}
.marker {
position: absolute;
top: 0;
bottom: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
width: var(--size-8);
font-size: var(--typo-size-3xl);
font-weight: var(--typo-weight-black);
color: var(--color-text-inverse);
background-color: var(--color-surface-inverse);
}
.content {
text-align: left;
}
.ref {
@mixin ml var(--spacing-snug);
@mixin anim-txt-characterglitch;
font-weight: var(--typo-weight-black);
color: var(--color-text-quarternary);
&:hover {
color: var(--color-primary);
}
}

View File

@@ -0,0 +1,33 @@
import type { Sidenote } from '@/lib/types/components';
import styles from './Container.module.css';
interface ContainerProps {
items: Sidenote[];
}
export default function Container({ items }: ContainerProps) {
return (
<aside className={styles.container}>
{items.map((sidenote: Sidenote) => (
<div
key={sidenote.id}
id={sidenote.id}
className={styles.wrapper}
style={{ positionAnchor: `--note-${sidenote.id}` }}
>
<div className={`${styles.note} ${styles[sidenote.type]}`}>
<span className={styles.marker}>
<span className={styles.symbol}>{sidenote.marker}</span>
</span>
<div className={styles.content}>
{sidenote.content}
<a href={`#ref-${sidenote.id}`} className={styles.ref}>
</a>
</div>
</div>
</div>
))}
</aside>
);
}

View File

@@ -0,0 +1,11 @@
.ref {
@mixin px var(--spacing-tight);
display: inline-block;
font-weight: var(--typo-weight-black);
transition: color 0.5s ease-in-out;
&:hover {
color: var(--color-primary);
}
}

View File

@@ -0,0 +1,17 @@
import type { Sidenote } from '@/lib/types/components';
import styles from './Item.module.css';
export default function SidenoteItem({ id, marker, content, type }: Sidenote) {
return (
<sup>
<a
href={`#${id}`}
className={styles.ref}
id={`#ref-${id}`}
style={{ anchorName: `--note-${id}` }}
>
[{marker}]
</a>
</sup>
);
}

View File

@@ -14,7 +14,7 @@
}
.inner {
@mixin responsive-wrapper;
@mixin layout-wrapper;
display: flex;
flex-direction: row;

View File

@@ -60,7 +60,8 @@
--area-bg-animation-keyframe: none;
--area-bg--animation-duration: 0s;
--area-bg--animation-timing: linear;
--area-bg-filter: grayscale(100%) contrast(150) brightness(150);
--area-bg-filter: grayscale(100%) contrast(10) brightness(250);
--area-bg-blendmode: color-dodge;
position: absolute;
z-index: -1;
@@ -70,6 +71,7 @@
object-fit: cover;
filter: var(--area-bg-filter);
mix-blend-mode: var(--area-bg-blendmode);
@media screen and (--bp-tablet-down) {
display: none;
@@ -93,27 +95,10 @@
--subtitle-transition: all 0.3s ease-in-out;
--title-transition: color 0.3s ease-in-out;
&::before {
content: "";
position: absolute;
z-index: -1;
width: 100%;
height: 100%;
opacity: 0;
background: alpharize(var(--grid-bg), 0.66);
}
&:hover {
--divider-color: var(--color-tertiary);
--title-color: var(--color-tertiary);
--subtitle-color: var(--color-tertiary);
&::before {
opacity: 1;
}
}
}
}

View File

@@ -2,6 +2,7 @@
.menu {
--grid-bg: var(--color-palette-charcoal-gray);
--grid-fg: var(--color-palette-light-silver);
--area-blendmode: normal;
/* === MenuTitle Vars === */
--title-color: var(--grid-fg);
@@ -40,13 +41,14 @@
--divider-width: var(--size-12);
--divider-height: var(--size-2);
--divider-font: var(--font-mono);
--divider-font-size: var(--typo-size-2xl);
--divider-line-height: 1;
--divider-font-size: var(--typo-size-3xl);
--divider-padding: 0 var(--typo-spacing-cozy);
--subtitle-font: var(--font-mono);
--subtitle-color: var(--grid-fg);
--subtitle-font-size: var(--typo-size-xl);
--subtitle-text-transform: uppercase;
--subtitle-letter-spacing: var(--typo-spacing-cozy);
--subtitle-letter-spacing: var(--typo-spacing-snug);
--subtitle-transition: none;
pointer-events: none;

View File

@@ -0,0 +1,13 @@
import { collection } from '@keystatic/core';
import { createArticleField } from '@/keystatic/fields/article';
export default collection({
label: 'Meta - Posts',
slugField: 'title',
path: 'content/meta/*',
format: { contentField: 'content' },
schema: {
...createArticleField('meta'),
},
});

View File

@@ -0,0 +1,46 @@
import { collection, fields } from '@keystatic/core';
export default collection({
label: 'Authors',
slugField: 'name',
path: 'content/taxonomy/authors/*',
format: { data: 'json' },
schema: {
name: fields.slug({ name: { label: 'Name' } }),
avatar: fields.image({
label: 'Avatar',
directory: 'public/images/authors',
publicPath: '/images/authors',
}),
description: fields.text({ label: 'Description', multiline: true }),
contacts: fields.array(
fields.object({
type: fields.select({
label: 'Contact Type',
defaultValue: 'email',
options: [
{
value: 'email',
label: 'E-Mail',
},
{
value: 'discord',
label: 'Discord',
},
{
value: 'other',
label: 'Other',
},
],
}),
url: fields.text({
label: 'URL',
}),
}),
{
label: 'Contact Type',
itemLabel: (props) => `${props.fields.type.value}`,
}
),
},
});

View File

@@ -0,0 +1,41 @@
import { collection, fields } from '@keystatic/core';
export default collection({
label: 'Tags',
slugField: 'name',
path: 'content/taxonomy/tags/*',
format: { data: 'json' },
schema: {
name: fields.slug({ name: { label: 'Name' } }),
icon: fields.conditional(
fields.select({
label: 'Icon Type',
defaultValue: 'none',
options: [
{
value: 'none',
label: 'None',
},
{
value: 'glyph',
label: 'Glyph',
},
{
value: 'img',
label: 'Image',
},
],
}),
{
none: fields.empty(),
glyph: fields.text({ label: 'Glyph' }),
img: fields.image({
label: 'icon',
publicPath: '/images/icons',
directory: 'public/images/icons',
}),
}
),
description: fields.text({ label: 'Description', multiline: true }),
},
});

View File

@@ -0,0 +1,61 @@
import { grid2X2Icon } from '@keystar/ui/icon/icons/grid2X2Icon';
import { fields } from '@keystatic/core';
import { repeating, wrapper } from '@keystatic/core/content-components';
export const gridComponents = {
Row: repeating({
label: 'Row',
icon: grid2X2Icon,
children: ['Column'], // This defines what can go inside
schema: {
style: fields.object({
cols: fields.integer({
label: 'Columns',
defaultValue: 2,
validation: { min: 2, max: 12 },
}),
gap: fields.text({
label: 'Gap',
description: 'Enter a variable or value',
}),
align: fields.select({
label: 'Align Items',
defaultValue: 'flex-start',
options: [
{ label: 'Start', value: 'flex-start' },
{ label: 'End', value: 'flex-end' },
{ label: 'Center', value: 'center' },
{ label: 'Baseline', value: 'baseline' },
{ label: 'Stretch', value: 'stretch' },
],
}),
justify: fields.select({
label: 'Justify Content',
defaultValue: 'flex-start',
options: [
{ label: 'Start', value: 'flex-start' },
{ label: 'End', value: 'flex-end' },
{ label: 'Center', value: 'center' },
{ label: 'Space Between', value: 'space-between' },
{ label: 'Space Around', value: 'space-around' },
{ label: 'Evenly', value: 'evenly' },
{ label: 'Stretch', value: 'stretch' },
{ label: 'Baseline', value: 'baseline' },
],
}),
}),
},
}),
Column: wrapper({
label: 'Column',
forSpecificLocations: true,
schema: {
style: fields.object({
colspan: fields.integer({
label: 'Colspan',
defaultValue: 1,
}),
}),
},
}),
};

View File

@@ -0,0 +1,4 @@
import { gridComponents } from '@/keystatic/components/general/grid';
import { sidenoteComponents } from '@/keystatic/components/general/sidenote';
export const generalComponents = { ...gridComponents, ...sidenoteComponents };

View File

@@ -0,0 +1,59 @@
import { panelRightDashedIcon } from '@keystar/ui/icon/icons/panelRightDashedIcon';
import { fields } from '@keystatic/core';
import { inline } from '@keystatic/core/content-components';
export const sidenoteComponents = {
Sidenote: inline({
label: 'Sidenote',
icon: panelRightDashedIcon,
schema: {
id: fields.text({
label: 'ID',
description: 'Unique Identifier (Auto-generated)',
}),
marker: fields.text({
label: 'Marker',
description: 'Number or Glyph for reference',
defaultValue: '⋄',
validation: {
length: {
min: 1,
max: 3,
},
},
}),
content: fields.text({
label: 'Note Content',
multiline: true,
validation: {
length: {
min: 1,
},
},
}),
type: fields.select({
label: 'Type',
description: 'Visual Style',
options: [
{
label: 'Default',
value: 'default',
},
{
label: 'Lore',
value: 'lore',
},
{
label: 'Crunch',
value: 'crunch',
},
{
label: 'Example',
value: 'example',
},
],
defaultValue: 'default',
}),
},
}),
};

View File

@@ -0,0 +1,26 @@
import { fields } from '@keystatic/core';
import { createContentField } from '@/keystatic/fields/content';
import { createSEOField } from '@/keystatic/fields/seo';
import { createMetaField } from '@/keystatic/fields/meta';
export const createArticleField = (imageSubfolder: string) => ({
title: fields.slug({ name: { label: 'Title' } }),
summary: fields.text({ label: 'Summary', multiline: true }),
cover: fields.object({
src: fields.image({
label: 'Cover Image',
directory: `public/images/covers/${imageSubfolder}`,
publicPath: `/images/covers/${imageSubfolder}`,
}),
alt: fields.text({ label: 'Alt' }),
caption: fields.text({ label: 'Caption', multiline: true }),
showInHeader: fields.checkbox({
label: 'Show in Header',
defaultValue: false,
}),
}),
meta: createMetaField(),
seo: createSEOField(),
content: createContentField(imageSubfolder),
});

View File

@@ -0,0 +1,22 @@
import { fields } from '@keystatic/core';
import type { ContentComponent } from '@keystatic/core/content-components';
import { generalComponents } from '@/keystatic/components/general';
export const createContentField = (
imageSubfolder: string,
additionalComponents?: Record<string, ContentComponent>
) =>
fields.markdoc({
label: 'Content',
options: {
image: {
directory: `public/images/content/${imageSubfolder}`,
publicPath: `/images/content/${imageSubfolder}`,
},
},
components: {
...generalComponents,
...additionalComponents,
},
});

View File

@@ -0,0 +1,44 @@
import type { ComponentSchema } from '@keystatic/core';
import { fields } from '@keystatic/core';
export const createMetaField = (): ComponentSchema =>
fields.object(
{
publicationDate: fields.datetime({
label: 'Publication Date',
defaultValue: { kind: 'now' },
}),
updateDate: fields.datetime({ label: 'Update Date' }),
status: fields.select({
label: 'Status',
defaultValue: 'draft',
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
{ label: 'Archived', value: 'archived' },
],
}),
isFeatured: fields.checkbox({
label: 'Featured',
description: 'Show on Homepage',
}),
tags: fields.array(
fields.relationship({
label: 'Tags',
collection: 'tags',
}),
{
label: 'Tags',
itemLabel: (props) => props.value || 'Select Tag',
}
),
author: fields.relationship({
label: 'Author',
collection: 'authors',
}),
},
{
label: 'Meta Information',
layout: [4, 4, 4, 12, 12, 12],
}
);

View File

@@ -0,0 +1,24 @@
import type { ComponentSchema } from '@keystatic/core';
import { fields } from '@keystatic/core';
export const createSEOField = (): ComponentSchema =>
fields.object(
{
title: fields.text({
label: 'SEO Title',
validation: { length: { max: 60 } },
}),
description: fields.text({
label: 'SEO Description',
multiline: true,
validation: { length: { max: 160 } },
}),
noIndex: fields.checkbox({
label: 'No Index',
description: 'Prevent search engines from indexing',
}),
},
{
label: 'SEO Settings',
}
);

7
src/lib/markdoc/nodes.ts Normal file
View File

@@ -0,0 +1,7 @@
import { Config } from '@markdoc/markdoc';
export const nodes: Config['nodes'] = {
document: {
render: '',
},
};

43
src/lib/markdoc/tags.ts Normal file
View File

@@ -0,0 +1,43 @@
import { Config } from '@markdoc/markdoc';
export const tags: Config['tags'] = {
Row: {
render: 'Row',
attributes: {
style: {
type: 'Object',
required: true,
},
},
},
Column: {
render: 'Column',
attributes: {
style: {
type: 'Object',
required: true,
},
},
},
Sidenote: {
render: 'Sidenote',
attributes: {
id: {
type: 'String',
required: false,
},
marker: {
type: 'String',
required: true,
},
content: {
type: 'String',
required: true,
},
type: {
type: 'String',
default: 'default',
},
},
},
};

34
src/lib/markdoc/utils.ts Normal file
View File

@@ -0,0 +1,34 @@
import type { RenderableTreeNode } from '@markdoc/markdoc';
import type { Sidenote } from '@/lib/types/components';
export function hasComponents(
renderable: RenderableTreeNode,
components: string[]
): boolean {
const renderablestring = JSON.stringify(renderable);
return components.some((component) => renderablestring.includes(component));
}
export function collectSideNotes(
node: RenderableTreeNode | RenderableTreeNode[]
): Sidenote[] {
const sidenotes: Sidenote[] = [];
function walk(n: any) {
if (!n) return;
if (Array.isArray(n)) {
n.forEach(walk);
} else if (n.name === 'Sidenote' && n.attributes) {
sidenotes.push({
id: n.attributes.id,
marker: n.attributes.marker,
content: n.attributes.content,
type: n.attributes.type || 'default',
});
}
if (n.children) walk(n.children);
}
walk(node);
return sidenotes;
}

View File

@@ -0,0 +1,14 @@
import { Metadata } from 'next';
import { ArticleContent } from '@/lib/types/content';
export default function generatePageMetaData(
article: ArticleContent
): Metadata {
const seo = article.seo;
return {
title: seo.title || article.title,
description: seo.description || article.summary,
robots: seo.noIndex,
};
}

View File

@@ -0,0 +1,15 @@
import { cache, reader } from '@/lib/readers/base';
export const getMetaPosts = cache(async () =>
reader.collections['meta_posts'].all()
);
export const getMetaPostBySlug = cache(async (slug: string) => {
const article = await reader.collections['meta_posts'].read(slug);
return article ? { ...article, slug } : null;
});
export const getMetaHome = cache(async () => {
const article = await reader.collections['meta_posts'].read('index');
return article ? { ...article } : null;
});

View File

@@ -0,0 +1,8 @@
import { cache, reader } from '@/lib/readers/base';
export const getAuthors = cache(async () => reader.collections.authors.all());
export const getAuthorBySlug = cache(async (slug: string) => {
const author = await reader.collections.authors.read(slug);
return author ? { ...author, slug } : null;
});

View File

@@ -0,0 +1,8 @@
import { cache, reader } from '@/lib/readers/base';
export const getTags = cache(async () => reader.collections.tags.all());
export const getTagBySlug = cache(async (slug: string) => {
const tag = await reader.collections.tags.read(slug);
return tag ? { ...tag, slug } : null;
});

View File

@@ -0,0 +1,6 @@
export interface Sidenote {
id: string;
marker: string;
content: string;
type: 'default' | 'lore' | 'crunch' | 'example';
}

3
src/lib/types/content.ts Normal file
View File

@@ -0,0 +1,3 @@
import { MetaContent } from '@/lib/types/meta';
export type ArticleContent = MetaContent;

8
src/lib/types/meta.ts Normal file
View File

@@ -0,0 +1,8 @@
import { Entry } from '@keystatic/core/reader';
import keystaticConfig from '~/keystatic.config';
export type MetaPost = Entry<
(typeof keystaticConfig.collections)['meta_posts']
>;
export type MetaContent = MetaPost;

View File

@@ -0,0 +1,8 @@
import { Entry } from '@keystatic/core/reader';
import keystaticConfig from '~/keystatic.config';
export type AuthorData = Entry<(typeof keystaticConfig.collections)['authors']>;
export type TagData = Entry<(typeof keystaticConfig.collections)['tags']>;
export type Author = AuthorData & { slug: string };
export type Tag = TagData & { slug: string };

24
src/lib/utils/date.ts Normal file
View File

@@ -0,0 +1,24 @@
export function toMilitaryDTG(datestring: string, timeZone = 'A') {
const date = new Date(datestring);
const day = date.getUTCDate().toString().padStart(2, '0');
const hours = date.getUTCHours().toString().padStart(2, '0');
const minutes = date.getUTCMinutes().toString().padStart(2, '0');
const months = [
'JAN',
'FEB',
'MAR',
'APR',
'MAY',
'JUN',
'JUL',
'AUG',
'SEP',
'OCT',
'NOV',
'DEC',
];
const month = months[date.getUTCMonth()];
const year = date.getUTCFullYear().toString().slice(-2);
return `${day}${hours}${minutes}${timeZone}${month}${year}`;
}

13
src/lib/utils/guards.ts Normal file
View File

@@ -0,0 +1,13 @@
export function isValidCover(cover: {
src: string | null;
alt: string;
caption: string;
showInHeader: boolean;
}): cover is {
src: string;
alt: string;
caption: string;
showInHeader: boolean;
} {
return cover.showInHeader && cover.src !== null;
}

View File

@@ -1,5 +1,7 @@
@layer content {
.content {
font-family: var(--font-body);
& h1 {
margin-block: var(--el-h1-vspace-top) var(--el-h1-vspace-bottom);
padding-bottom: var(--spacing-snug);
@@ -430,19 +432,21 @@
}
a {
@mixin anim-txt-pixelshift 0.6s steps(15, end), var(--color-primary);
&:not([class]) {
@mixin anim-txt-typewriter;
color: var(--color-text-tertiary);
transition: color 0.2s ease-in-out;
color: var(--color-text-tertiary);
transition: color 0.2s ease-in-out;
&:hover {
cursor: pointer;
color: var(--color-primary);
}
&:hover {
cursor: pointer;
color: var(--color-primary);
}
&:visited,
&:active {
color: var(--color-primary-emphasis);
&:visited,
&:active {
color: var(--color-primary-emphasis);
}
}
}

View File

@@ -4,4 +4,6 @@
@custom-media --bp-tablet-up (width >= 48rem);
@custom-media --bp-tablet-down (width < 80rem);
@custom-media --bp-desktop (width >= 80rem);
@custom-media --max-layout (width <= 92rem);
@custom-media --bp-margin (width >= 64rem);
}

View File

@@ -29,8 +29,8 @@
--size-192: 48rem;
--size-256: 64rem;
--size-320: 80rem;
--size-384: 96rem;
--size-360: 90rem;
--size-384: 96rem;
--size-400: 100rem;
--size-480: 120rem;
@@ -91,5 +91,9 @@
--spacing-generous: var(--size-16);
--spacing-luxurious: var(--size-24);
--spacing-expansive: var(--size-32);
/* == Responsive Content Dimensions */
--layout-max-width: 1280px;
--content-max-width: 75ch;
}
}

View File

@@ -9,7 +9,7 @@
--font-mono: var(--font-iosevka-mono);
/* == Font Sizes == */
--typo-size-responsive: clamp(1rem, 2.5vw, 1.25rem);
--typo-size-responsive: clamp(1rem, 2.5vw, 1.5rem);
--typo-size-base: 16px;
--typo-size-8xl: 8em;
--typo-size-7xl: 6.375em;
@@ -68,6 +68,5 @@
--vspace-comfortable: 1.75;
--vspace-loose: 2;
--vspace-spacious: 2.5;
}
}

View File

@@ -5,4 +5,16 @@
padding: $vspacing $hspacing;
font-family: var(--font-body);
font-size: $fontSize;
}
@define-mixin layout-wrapper{
@mixin mx auto;
width: 100%;
max-width: var(--layout-max-width);
@media screen and (--max-layout) {
@mixin px var(--spacing-comfortable);
}
}