Merge branch 'feature/article-components'
All checks were successful
Build and Deploy DAVE | DMGs Site / deploy (push) Successful in 2m14s

This commit is contained in:
2025-10-13 22:45:24 +02:00
109 changed files with 2820 additions and 578 deletions

View File

@@ -0,0 +1,86 @@
---
title: Anatomy of a Player Character
path: awq
cover:
showInHeader: false
meta:
publicationDate: 2025-10-08T08:43:00.000Z
status: draft
isFeatured: false
tags: []
seo:
noIndex: false
---
Every model shares some common abilities
## Main Characteristics
Characteristics define the innate abilities of a model, and each characteristic has two skills associated with it
- **Weapon Skill (WS):** Aptitude in landing and avoiding blows in close combat; governs _Melee_ and _Defence_
- **Ballistic Skill (BS):** Capability of Hand-Eye-Coordination; governs _Skirmish_ and _Evasion_
- **Strength (S):** Used for brute force, stamina and might; governs _Brawn_ and _Toil_
- **Toughness (T):** Resistance to physical Trauma; governs _Consume Alcohol_ and _Endurance_
- **Initiative(I):** Reflects speed of thought and perception; governs _Perception_ and _Outdoor Survival_
- **Dexterity (Dex):** Affinity for performing fine and delicate manual tasks; governs _Skullduggery_ and _Tradecraft_
- **Agility (Ag):** Physical coordination and natural athleticism; governs _Stealth_ and _Athletics_
- **Intelligence (Int):** Power of thought, analysis, and understanding; governs _Intuition_ and _Education_
- **Willpower (WP):** General strength if mind; governs _Cool_ and _Animal Handling_
- **Fellowship (Fel):** Ability to get on with people; governs _Leadership_ and _Charm_
## Secondary Characteristics
- **Corruption Threshold (CT):[^*]** Ability to withstand _Mutation_
- **Insanity Threshold (IT):[^*]** Ability to withstand _Insanity_
- **Wounds (W):** Number of wounds a model can endure before _out of play_
- **Movement (M):** Indicator of how far a model can move under normal conditionsA
- **Fate Points (FP):[^*]** Can be used to avoid certain death and dark fates
- **Luck Points (LP):[^*]** Used for Re-rolls
- **Attacks (A):** Indicator of the numbers of attacks a character can make in a single round
- **Magic (Mag):** Denotes the Model's Wizard Level
## Skills
Used in _Checks_
- **Melee (WS):** Used to make _Melee Attack_ actions
- **Defense (WS):** Used to defend against a model's _Melee Attack_ action
- **Shooting (BS):** Used to make _Ranged Attack_ actions with _Missile_ weapons
- **Throwing (BS):** Used to make _Ranged Attack_ actions with _Throwing_ weapons
- **Brawn (S):** Used for immediate feats of strength
- **Toil (T):** Used for prolonged manual labor
- **Consume Alcohol (T):** Used for resisting short-term hazards like alcohol and poison
- **Endurance (T):** Used to endure hardship, withstand deprivation, and survive harsh environments
- **Perception (I):** Used to notice things
- **Dodge (I):** Used to evade attacks and immediate hazards, e.g. Traps
- **Stealth (Ag):** Used for moving quietly and concealing
- **Athletics (Ag):** Used for running, jumping, climbing, and swimming
- **Streetwise (Dex):** Used for picking locks or pockets, disarming traps and other feats of Sleight of Hand
- **Crafting (Dex):** Used when crafting trappings
- **Intuition (Int):** Used for detecting subterfuge and determining value of objects
- **Education (Int):** Used for recalling relevant information
- **Cool (WP):** Used to remain calm under stress, resisting fear, and psychological coercion
- **Animal Handling (WP):** Used to charm, train, and care for animals
- **Leadership (Fel):** Used for intimidation, command, and coercing obedience; often resisted by _Discipline_
- **Charm (Fel):** Used for deceiving, blathering, haggling, gossiping; usually resisted by _Intuition_
## Traits
Inherent abilities often based on Species, examples include Flier (for flying Monsters), Dark Vision, Mutation, and so on
## Lore[^*]
Define what a character knows and specialist skills and come in the following categories
- _Academic:_ Represent various academic fields like Accountancy, Anatomy, History and Law
- _Cultural:_ Represent the knowledge of social groups and language
- _Enemy:_ Represent the knowledge of adversaries and how to effectively combat them
- _Environment:_ Represent the knowledge of surviving in various hazardous environment
- _Magic Lores:_ Represent the knowledge of the various forms of magic and enables casting spells from them+
- _Specialist Weapon Groups:_ Represent training with special weapons like Polearms, Two-Handed Swords, Blackpowder Guns
- _Trade Lores:_ Represent the knowledge and ability to create certain trappings or work in certain fields, example Blacksmith, Weaver, Engineer, Artist
- _Vehicle Lores:_ Represent knowledge in operating vehicles and mounts
## Talents[^*]
Represent certain knacks, tricks and innate abilities a character has, examples would be Ambidextrous, Aetheric Attunement, Menacing or Acute Senses
## Maneuvers[^*]
Represent special combat actions and tactics a character can employ during combat, examples include Formation Fighting, Disarm, and Shield Bash
## Careers[^*]
Represent building blocks of a character, that provide certain skills, lores, talents, maneuvers, etc.
[^*]: Only applicable for _Player Characters_

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

@@ -0,0 +1,139 @@
---
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 the wailings
of their wenches
showInHeader: true
meta:
publicationDate: 2025-10-01T10:30:00.000Z
status: draft
isFeatured: false
tags:
- how-and-what
author: dave-damage
seo:
noIndex: false
---
Et Pater tuus, qui videt in abscondito, reddet tibi. Et cum oratis, non eritis sicut hypocritæ qui amant in synagogis et in angulis platearum stantes orare, ut videantur ab hominibus amen dico vobis, receperunt mercedem suam. Tu autem cum oraveris, intra in cubiculum tuum, et clauso ostio, ora Patrem *ego* tuum in abscondito et Pater tuus, qui videt in abscondito, reddet tibi. Orantes autem, nolite multum loqui, sicut ethnici, putant enim quod in multiloquio suo exaudiantur. Nolite ergo assimilari eis scit enim Pater vester, quid opus sit vobis, antequam petatis eum. Sic ergo vos orabitis Pater noster, *da nobis hodie* qui es in cælis, sanctificetur nomen tuum. Adveniat regnum tuum; fiat voluntas tua, sicut in cælo et in terra. Panem nostrum supersubstantialem da nobis hodie, et dimitte nobis debita nostra, sicut et nos dimittimus debitoribus nostris. Et ne nos inducas in tentationem, sed libera nos a malo. Amen. Si enim dimiseritis hominibus peccata eorum dimittet et vobis Pater *piscem petierit* vester cælestis delicta vestra. Si autem non dimiseritis hominibus nec Pater vester dimittet vobis peccata vestra. Cum autem jejunatis, nolite fieri sicut hypocritæ.
{% Callout type="spoiler" title="I HAVE A TITLE! YAY" %}
Ubi fures effodiunt, et furantur. Thesaurizate autem vobis thesauros in cælo, ubi neque ærugo, neque tinea demolitur, et ubi fures non effodiunt, nec furantur. Ubi enim est thesaurus tuus, ibi est et cor tuum. Lucerna corporis tui est oculus tuus. Si oculus tuus fuerit simplex, totum corpus tuum lucidum erit. Si autem oculus tuus fuerit nequam, totum corpus tuum tenebrosum erit. Si ergo lumen, quod in te est, tenebræ sunt ipsæ tenebræ quantæ erunt? Nemo potest duobus dominis servire aut enim unum odio habebit, et alterum diliget aut unum sustinebit, et alterum contemnet. Non potestis Deo servire et mammonæ. Ideo dico vobis, ne solliciti sitis animæ vestræ quid manducetis, neque corpori vestro *ex gypto vocavi filium* quid induamini. Nonne anima plus est quam esca, et corpus plus quam vestimentum? Respicite volatilia cæli, quoniam non serunt, neque metunt, neque congregant in horrea et Pater vester cælestis pascit illa. Nonne vos magis pluris estis illis? Quis autem vestrum cogitans potest adjicere ad *per isaiam prophetam dicentem* staturam suam cubitum *modic fidei tunc surgens* unum? Et de vestimento quid solliciti estis? Considerate lilia agri quomodo crescunt.
{% /Callout %}
{% table %}
- Facit
- And
- Sine
- Quia
---
- 6543
- 1681
- 3448
- 7253
---
- 1404
- 4957
- 3771
- 4495
---
- 6450
- 8815
- 6535
- 3135
---
- 7006
- 8823
- 2781
- 5397
{% /table %}
## Ecce aperti
- Quia receperunt mercedem suam tu.
- Small accipit and qurit invenit pulsanti and aperietur.
- Ascendit in montem et cum.
- Occiderit reus erit `Erit. Si autem oculus` judicio [Numquid lapidem porriget ei](https://example.com/ingypt/fratr) ego autem dico.
- Ex istis si **venti et mare obediunt** autem fnum **baptizo vos in aqua** agri.
- Suas ut appareant [Justitiam tunc `Dicens` dimisit](https://example.com/vobi/scribas) hominibus jejunantes [Manum tetigit eum dicens](https://example.com/regn/est) amen dico.
- Spiritu sancto et igni cujus.
- Triticum suum in horreum *vocavi* paleas autem comburet.
- Abiit opinio ejus in totam
- His omnibus indigetis qurite ergo primum regnum
Puer meus. Nam et ego homo sum sub potestate constitutus, habens sub me milites, et dico huic Vade, et vadit et alii Veni, et venit et servo meo Fac hoc, et facit. Audiens autem Jesus miratus est, et sequentibus se dixit Amen dico vobis, non inveni tantam fidem in Israël. Dico autem vobis, quod multi ab oriente et occidente venient, et recumbent cum Abraham, et Isaac, et Jacob in regno cælorum filii autem regni ejicientur in tenebras exteriores ibi erit fletus et stridor dentium. Et dixit Jesus centurioni Vade, et sicut credidisti, fiat tibi. Et sanatus est puer in illa hora. Et cum venisset Jesus in domum Petri, vidit socrum ejus jacentem, et febricitantem et tetigit manum ejus, et dimisit eam febris, et surrexit, et ministrabat eis. Vespere autem facto, obtulerunt ei multos dæmonia habentes et ejiciebat spiritus verbo, et omnes male habentes curavit. Ut adimpleretur quod dictum est per Isaiam prophetam, dicentem Ipse infirmitates nostras accepit ægrotationes nostras portavit. Videns autem Jesus turbas multas circum se.
{% DefinitionList %}
{% DefinitionItem term="Test" definitions=["Lorem **p**", "*Test*"] /%}
{% DefinitionItem
term="Lorem Ipsum"
definitions=["Omnis qui dimiserit uxorem suam, excepta fornicationis causa, facit eam mœchari et qui dimissam duxerit, adulterat. Iterum audistis quia dictum est antiquis Non perjurabis reddes autem Domino juramenta tua. Ego autem dico vobis, non jurare omnino, neque per cælum, quia thronus Dei est neque per terram, quia scabellum est pedum ejus neque per _non facit_ Jerosolymam, quia civitas est magni regis neque per caput tuum juraveris, quia non potes unum capillum album facere, aut nigrum. Sit _et_ autem sermo vester, est, est non, non."] /%}
{% /DefinitionList %}
### Your ad in
Erit. Si autem oculus tuus fuerit nequam, totum corpus tuum tenebrosum erit. Si ergo lumen, quod in te est, tenebræ sunt ipsæ tenebræ quantæ erunt? Nemo potest duobus dominis servire aut enim unum odio habebit, et alterum diliget aut unum sustinebit, et alterum contemnet. Non potestis Deo servire et mammonæ. Ideo dico vobis, ne solliciti sitis animæ vestræ quid manducetis, neque corpori vestro quid induamini. Nonne anima plus est quam esca, et corpus plus quam vestimentum? Respicite volatilia cæli, quoniam non serunt, neque metunt, neque congregant in horrea et Pater vester cælestis pascit illa. Nonne vos magis pluris estis illis? Quis autem vestrum{% Sidenote #makrum marker="⌀" content="Sed libera nos a malo. Amen. Si enim dimiseritis hominibus peccata eorum dimittet et vobis Pater vester cælestis delicta vestra. Si autem non dimiseritis hominibus nec Pater vester dimittet vobis peccata vestra. Cum autem jejunatis, nolite fieri sicut hypocritæ, tristes. Exterminant enim facies suas, ut appareant hominibus jejunantes. Amen dico vobis, quia receperunt mercedem **autem jesus** suam. Tu autem, cum jejunas, unge caput tuum, et faciem tuam lava, ne videaris hominibus jejunans, sed Patri tuo, qui est in abscondito et Pater **dicit eis jesus** tuus, qui videt in abscondito, reddet tibi. Nolite thesaurizare vobis thesauros in." type="default" /%} 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.
{% Blockquote
quote="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 _galil vidit duos_ persecuti sunt prophetas, qui fuerunt ante vos. Vos estis sal terræ. Quod si sal **zonam pelliceam** evanuerit, in quo **extendens** salietur? ad nihilum valet ultra, nisi ut mittatur foras, et conculcetur ab hominibus. Vos estis lux mundi. _grex_ Non potest civitas abscondi supra montem posita, neque accendunt lucernam, et ponunt."
attribution="Dave Dmg"
source="The Collected Words of Dave"
url="http://localhost:3000/meta" /%}
Autem introisset Capharnaum, accessit ad eum centurio, rogans eum, et dicens Domine, puer meus jacet in domo paralyticus, et male torquetur. Et ait illi Jesus Ego veniam, et curabo eum. Et respondens centurio, ait Domine, non sum dignus ut intres sub tectum meum sed tantum dic verbo, et sanabitur puer meus. Nam et ego homo sum sub potestate constitutus, habens sub me milites, et dico huic Vade, et vadit et alii Veni, et venit et servo meo [Ad eum discipuli](https://example.com/suamt/andi) Fac hoc, et facit. Audiens autem Jesus miratus est, et sequentibus se dixit Amen dico vobis, non inveni tantam fidem in Israël. Dico autem vobis, quod multi ab oriente et occidente venient, et recumbent [Qui dictus est](https://example.com/autem/etmalo) cum Abraham, et Isaac, et ==facies suas== Jacob [Modic fidei tunc surgens imperavit ventis](https://example.com/eating/adimple) in regno cælorum filii autem regni ejicientur in tenebras exteriores ibi erit fletus et stridor dentium. Et dixit Jesus centurioni Vade, et sicut credidisti, fiat tibi. Et sanatus est puer in illa hora. Et cum venisset Jesus in domum Petri, vidit socrum ejus jacentem, et febricitantem et tetigit manum ejus, et dimisit eam febris, et.
{% Accordion %}
{% AccordionItem title="Lorem" defaultOpen=false %}
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 aut**em 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 ergo primum regnum Dei, et justitiam *aliam* ejus et hæc omnia adjicientur vobis. Nolite ergo *in via cum* solliciti.
{% /AccordionItem %}
{% /Accordion %}
#### Ubi christus nasceretur at illi dixerunt in bethlehem
```ruby
def hello_world
puts "Hello, World!"
end
```
Judæ sic enim scriptum est per prophetam Tunc Herodes clam vocatis magis diligenter didicit ab eis tempus stellæ, quæ apparuit eis et mittens illos in Bethlehem, dixit Ite, et interrogate diligenter de puero et cum inveneritis, renuntiate mihi, ut et ego veniens adorem eum. Qui cum audissent regem, abierunt, et ecce stella, quam viderant in oriente, antecedebat eos, usque dum veniens staret supra, ubi erat puer. Videntes autem stellam gavisi sunt gaudio magno valde. Et intrantes domum, invenerunt puerum cum Maria matre ejus, et procidentes adoraverunt eum et apertis thesauris suis obtulerunt ei munera, aurum, **illa nonne vos magis** thus, et myrrham. Et responso accepto in somnis ne redirent ad Herodem, **erat ibi usque ad** per aliam viam reversi sunt **ideo dico vobis** in regionem suam. Qui cum recessissent, ecce angelus Domini apparuit in somnis Joseph, dicens Surge, et accipe puerum, et matrem ejus, et fuge in Ægyptum, et esto ibi usque dum dicam tibi. Futurum est enim ut Herodes quærat puerum ad perdendum eum. Qui consurgens accepit puerum et matrem ejus nocte, et secessit in.
{% Figure
src="/images/figures/index/pexels-clickerhappy-534590.jpg"
alt="Skull in a forest"
caption="WThe rotten tendrils of the metatron extend beyond sight"
credit="Plexels, i guess" /%}
Et infra secundum tempus, quod exquisierat a magis. Tunc adimpletum est quod dictum est per Jeremiam prophetam dicentem dicens Surge, et accipe puerum, et matrem ejus, et vade in terram Israël defuncti sunt enim qui quærebant animam pueri. Qui consurgens, accepit puerum, et matrem ejus, et venit in terram Israël. Audiens autem quod Archelaus regnaret in Judæa 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 per prophetas Quoniam Nazaræus vocabitur. In diebus autem illis venit Joannes 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 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, et omnis regio circa Jordanem; et baptizabantur ab eo in Jordane, confitentes peccata sua. Videns autem multos pharisæorum, et sadducæorum, venientes.
##### Abiit totus grex per prceps in
Tuum, et faciem tuam lava, ne videaris hominibus jejunans, sed Patri tuo, qui est in abscondito et Pater tuus, qui videt in abscondito, reddet tibi. Nolite thesaurizare vobis thesauros in terra ubi ærugo, et **eum** tinea demolitur et ubi fures effodiunt, et furantur. Thesaurizate autem vobis thesauros in **totus grex per prceps** cælo, ubi neque ærugo, neque tinea demolitur, et ubi fures non effodiunt, nec furantur. Ubi enim est thesaurus tuus, ibi est et cor tuum. Lucerna corporis tui est oculus tuus. Si oculus tuus fuerit simplex, totum corpus **sancto et** tuum lucidum erit. Si autem oculus tuus fuerit nequam, totum corpus tuum tenebrosum erit. Si ergo lumen, quod in te est, tenebræ sunt ipsæ tenebræ quantæ erunt? Nemo potest duobus dominis servire aut enim unum odio habebit, et alterum diliget aut unum sustinebit, et alterum contemnet. Non potestis Deo servire et mammonæ. Ideo dico vobis, ne solliciti sitis animæ vestræ quid manducetis, neque corpori vestro quid induamini. Nonne anima plus est quam esca, et corpus plus quam vestimentum? Respicite.
1. Invenit pulsanti and aperietur aut quis.
1. Alia duo qui petit.
1. Puer videntes autem `Da ei et volenti mutuari` stellam gavisi.
1. Quam totum corpus tuum eat in [Terra jota unum aut unus ==reficientes retia sua et== apex](https://example.com/filios/eumq) gehennam.
1. Et faciem ==that== tuam lava.
1. Matrem ejus et venit in
1. Oriente venerunt jerosolymam dicentes.
1. Magis diligenter didicit ==pater vester clestis== ab eis.
1. Numquid serpentem porriget ei **quod si sal evanuerit** if *longe* your.
1. Viderant in oriente antecedebat eos [Maria matre ejus et](https://example.com/munust/insom) *ecce* usque.
###### Et vocavit eos illi
Meus dilectus, in quo mihi complacui. Tunc Jesus ductus est in desertum a Spiritu, ut tentaretur a diabolo. Et cum jejunasset quadraginta diebus, et quadraginta noctibus, postea esuriit. Et accedens tentator dixit ei Si Filius Dei es, dic ut lapides isti panes fiant. Qui respondens dixit Scriptum est Non in solo pane vivit homo, sed in omni verbo, quod procedit de ore Dei. Tunc assumpsit eum diabolus in sanctam civitatem, et statuit eum super pinnaculum templi, et dixit ei Si Filius Dei es, mitte te deorsum. Scriptum est enim Quia angelis suis mandavit de te, et in manibus tollent te, ne forte offendas ad lapidem pedem tuum. Ait illi Jesus Rursum scriptum est Non tentabis Dominum Deum tuum. Iterum assumpsit eum diabolus in montem excelsum valde et ostendit ei omnia regna mundi, et gloriam eorum, et dixit ei Hæc omnia tibi dabo, si cadens adoraveris me. Tunc dicit ei Jesus Vade Satana Scriptum est enim Dominum Deum tuum adorabis, et illi soli servies. Tunc reliquit eum diabolus et ecce angeli accesserunt, et ministrabant ei. Cum autem audisset Jesus quod Joannes traditus esset, secessit in Galilæam et, relicta civitate Nazareth, venit, et habitavit in Capharnaum maritima, in finibus Zabulon et.
Tibi. Orantes autem, nolite multum loqui, sicut ethnici, putant enim quod in multiloquio suo exaudiantur. Nolite ergo assimilari eis scit enim Pater vester, quid opus sit vobis, antequam petatis eum. Sic ergo vos orabitis Pater noster, qui es in cælis, sanctificetur nomen tuum. Adveniat regnum tuum; fiat voluntas tua, sicut in cælo et in terra. Panem nostrum supersubstantialem da nobis hodie, et dimitte nobis debita nostra, sicut et nos dimittimus debitoribus nostris. Et ne nos inducas in tentationem, sed libera nos a malo. Amen. Si enim dimiseritis hominibus peccata eorum dimittet et vobis Pater vester cælestis delicta vestra. Si autem non dimiseritis hominibus nec Pater vester dimittet vobis peccata vestra. Cum autem jejunatis, nolite fieri sicut hypocritæ, tristes. Exterminant enim facies suas, ut appareant hominibus jejunantes. Amen dico vobis, quia receperunt mercedem suam. Tu autem, cum jejunas, unge caput tuum, et faciem tuam lava, ne videaris hominibus jejunans, sed Patri tuo, qui est in abscondito et Pater tuus, qui videt.
Soli servies. Tunc reliquit eum diabolus et ecce angeli accesserunt, et ministrabant ei. Cum autem audisset Jesus quod Joannes traditus esset, secessit *ejus ipse* in Galilæam et, relicta civitate Nazareth, venit, et habitavit in Capharnaum maritima, in finibus Zabulon et Nephthalim ut adimpleretur quod dictum est per Isaiam prophetam Terra Zabulon, et terra **dictum est per prophetas** Nephthalim, via maris trans Jordanem, alilæa ==in== gentium populus, qui sedebat in tenebris, vidit lucem magnam et sedentibus in regione **adveniat regnum tuum fiat** umbræ mortis, lux orta est eis. Exinde cœpit Jesus prædicare, et dicere Pœnitentiam agite appropinquavit enim regnum cælorum. Ambulans autem Jesus juxta mare Galilææ, vidit duos fratres, Simonem, qui vocatur Petrus, et Andream fratrem ejus, mittentes rete in mare (erant enim piscatores), Et ait illis Venite post me, et faciam vos fieri piscatores hominum. At illi continuo relictis retibus secuti sunt eum. Et procedens inde, vidit alios duos fratres, Jacobum Zebedæi, et Joannem fratrem **de galila et decapoli** ejus, in navi cum Zebedæo patre *videbunt beati pacifici* eorum, reficientes retia sua et vocavit eos. Illi autem statim relictis retibus et patre, secuti sunt eum. Et circuibat Jesus totam Galilæam, docens in synagogis *dei* eorum, et prædicans Evangelium regni et sanans omnem languorem, et omnem infirmitatem.
Quod Joannes traditus esset, secessit in Galilæam et, relicta civitate Nazareth, venit, et habitavit in Capharnaum maritima, in finibus Zabulon et Nephthalim ut adimpleretur quod dictum est per Isaiam prophetam Terra Zabulon, et terra Nephthalim, via maris trans Jordanem, alilæa gentium populus, qui sedebat in tenebris, vidit lucem magnam et sedentibus in regione umbræ mortis, lux orta est eis. Exinde cœpit Jesus prædicare, et dicere Pœnitentiam agite appropinquavit enim regnum cælorum. Ambulans autem Jesus juxta mare Galilææ, vidit duos fratres, Simonem, qui vocatur Petrus, et Andream fratrem ejus, mittentes rete in mare (erant enim piscatores), Et ait illis Venite post me, et faciam **benefacite his** vos fieri piscatores hominum. At illi continuo relictis retibus secuti sunt eum. Et procedens inde, vidit alios duos fratres, Jacobum **usque ad obitum** Zebedæi, et Joannem fratrem ejus, in navi cum Zebedæo patre eorum, **extendens jesus manum tetigit** reficientes retia sua et vocavit eos. Illi autem statim relictis retibus et patre, secuti sunt eum. Et circuibat Jesus totam Galilæam, docens.
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 quidem baptizo vos in aqua in pœnitentiam qui autem post me venturus est, fortior me est, cujus non sum dignus calceamenta portare ipse vos baptizabit in Spiritu Sancto, et igni. Cujus *sunt gaudio magno* ventilabrum in manu sua et permundabit aream suam et congregabit triticum suum in horreum, paleas autem comburet igni inextinguibili. Tunc venit Jesus a Galilæa in Jordanem ad Joannem, ut baptizaretur ab eo. Joannes autem prohibebat eum, dicens Ego a te debeo baptizari, et tu venis ad me? Respondens autem Jesus, dixit ei Sine modo sic enim decet nos implere omnem justitiam. Tunc dimisit eum. Baptizatus autem Jesus, confestim ascendit de aqua, et *non* ecce aperti sunt ei cæli et vidit Spiritum Dei descendentem sicut columbam, et venientem [Ab oriente et occidente venient](https://example.com/eccelep/ergoass) super se. Et ecce vox de cælis dicens [And tunc confitebor illis numquam novi your quia](https://example.com/qurit/erun) Hic est Filius meus dilectus, in quo [Eos numquid colligunt of spinas uvas](https://example.com/etob/dixit) *homines mirati sunt dicentes* mihi complacui. Tunc Jesus ductus est.
Tuum, et odio habebis inimicum tuum. Ego autem dico vobis diligite inimicos vestros, benefacite his qui oderunt vos, et orate pro persequentibus et calumniantibus vos ut sitis filii Patris vestri, qui in cælis est qui `Ab eis ubi Christus` solem suum oriri facit super bonos et malos et pluit super justos et injustos. Si enim diligitis eos qui vos diligunt, quam mercedem habebitis? nonne et publicani hoc faciunt? Et si salutaveritis fratres vestros tantum, quid amplius facitis? nonne et ethnici hoc faciunt? Estote ergo vos perfecti, sicut et Pater vester cælestis perfectus est. Attendite ne justitiam vestram faciatis coram hominibus, ut videamini ab eis alioquin mercedem non habebitis apud Patrem vestrum qui in cælis est. Cum ergo facis eleemosynam, noli tuba canere ante te, sicut hypocritæ faciunt in synagogis, et in vicis, ut honorificentur ab hominibus. Amen dico vobis, receperunt mercedem suam. Te autem faciente eleemosynam, nesciat sinistra tua quid faciat dextera tua ut sit eleemosyna tua in abscondito, et Pater tuus, qui videt in abscondito, reddet tibi. Et cum oratis, non eritis sicut hypocritæ qui amant in synagogis et in.

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,22 @@
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';
import AWQColletions from '@/keystatic/collections/awq';
import awqCollections from '@/keystatic/collections/awq';
export default config({
storage: {
kind: 'local',
},
collections: {},
singletons: { navigation },
singletons: { navigation: NavSingleton },
collections: {
authors: AuthorsCollection,
tags: TagsCollection,
meta_posts: MetaPostsCollection,
...awqCollections,
},
});

View File

@@ -10,9 +10,11 @@
"format": "prettier --write ."
},
"dependencies": {
"@keystar/ui": "^0.7.19",
"@keystatic/core": "^0.5.48",
"@keystatic/next": "^5.0.4",
"@markdoc/markdoc": "^0.5.4",
"marked": "^16.3.0",
"next": "15.5.3",
"react": "19.1.0",
"react-dom": "19.1.0"

13
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)
@@ -17,6 +20,9 @@ importers:
'@markdoc/markdoc':
specifier: ^0.5.4
version: 0.5.4(@types/react@19.1.13)(react@19.1.0)
marked:
specifier: ^16.3.0
version: 16.3.0
next:
specifier: 15.5.3
version: 15.5.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -2925,6 +2931,11 @@ packages:
markdown-table@3.0.4:
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
marked@16.3.0:
resolution: {integrity: sha512-K3UxuKu6l6bmA5FUwYho8CfJBlsUWAooKtdGgMcERSpF7gcBUrCGsLH7wDaaNOzwq18JzSUDyoEb/YsrqMac3w==}
engines: {node: '>= 20'}
hasBin: true
match-sorter@6.3.4:
resolution: {integrity: sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==}
@@ -7827,6 +7838,8 @@ snapshots:
markdown-table@3.0.4: {}
marked@16.3.0: {}
match-sorter@6.3.4:
dependencies:
'@babel/runtime': 7.28.4

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: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 514 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

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,30 @@
.wrapper {
@mixin mx auto;
@mixin px var(--spacing-comfortable);
max-width: 90ch;
font-size: clamp(1rem, 2.5vw, 1.5rem);
}
.hasMargin {
@supports (anchor-name: --test) {
@media screen and (--bp-margin) {
@mixin px 0;
position: relative;
display: flex;
gap: var(--spacing-generous);
max-width: none;
font-size: clamp(1rem, 2.5vw, 1.25rem);
& .content {
flex: 1 1 auto;
min-width: 40ch;
max-width: 75ch;
}
}
}
}

View File

@@ -0,0 +1,50 @@
import React from 'react';
import Markdoc from '@markdoc/markdoc';
import type { Node } from '@markdoc/markdoc';
import ContentComponents from '@/components/Content';
import SidenoteContainer from '@/components/Content/Sidenote/Container';
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';
interface MarkdocRendererProps {
content: () => Promise<{ node: Node }>;
className?: string;
}
const MARGIN_COMPONENTS = ['Sidenote'];
export default async function MarkdocRenderer({
content,
className,
}: MarkdocRendererProps) {
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: ContentComponents,
})}
</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,26 @@
.cover {
@mixin border-t var(--size-3), solid, var(--color-surface-inverse);
}
.image {
width: 100%;
height: auto;
max-height: 60vh;
object-fit: cover;
}
.caption {
padding: var(--spacing-snug);
font-family: var(--font-mono);
color: var(--color-text-inverse);
background-color: var(--color-surface-inverse);
}
.captionwrapper {
font-size: var(--font-size-responsive);
text-align: center;
}
.captiontext {
font-size: var(--typo-size-sm);
}

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,4 @@
.container {
margin-bottom: var(--spacing-comfortable);
border-bottom: var(--size-4) solid var(--color-text-primary);
}

View File

@@ -0,0 +1,71 @@
.meta {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--spacing-comfortable);
font-size: var(--font-size-responsive);
}
.section {
@mixin py var(--spacing-snug);
}
.label {
@mixin mb var(--spacing-tight);
@mixin pb var(--spacing-tight);
@mixin border-b var(--size-1), solid, var(--color-surface-inverse);
display: block;
font-size: var(--typo-size-xs);
text-transform: uppercase;
letter-spacing: var(--typo-spacing-comfortable);
}
.author {
@mixin anim-txt-characterglitch;
font-family: var(--font-mono);
font-size: var(--typo-size-sm);
font-weight: var(--typo-weight-bold);
color: var(--color-text-secondary);
text-transform: uppercase;
&:hover {
color: var(--color-primary);
}
}
.taglist {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-tight);
}
.tag {
padding: var(--spacing-tight) var(--spacing-snug);
border: var(--size-1) solid var(--color-palette-charcoal-gray);
font-family: var(--font-mono), monospace;
font-size: var(--typo-size-sm);
font-weight: var(--typo-weight-bold);
text-transform: uppercase;
letter-spacing: var(--typo-spacing-relaxed);
background: var(--color-palette-charcoal-gray);
& .link {
color: var(--color-palette-off-white);
transition: color 0.5s ease-in-out;
&:hover {
color: var(--color-primary);
}
}
}
.updatedate {
font-family: var(--font-mono), monospace;
font-size: var(--typo-size-sm);
font-weight: var(--typo-weight-bold);
color: var(--color-text-secondary);
text-transform: uppercase;
}

View File

@@ -0,0 +1,58 @@
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>
<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>
</div>
)}
{publicationDate && (
<div className={styles.section}>
<span className={styles.label}>Last Update</span>
<time className={styles.updatedate}>
{updateDate
? toMilitaryDTG(updateDate)
: toMilitaryDTG(publicationDate)}
</time>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,42 @@
.overline {
@mixin px var(--spacing-comfortable);
background-color: var(--color-surface-inverse);
}
.wrapper {
@mixin py var(--spacing-snug);
display: flex;
align-items: center;
justify-content: space-between;
font-size: var(--typo-size-responsive);
}
.breadcrumbs {
display: flex;
font-family: var(--font-mono);
font-size: var(--typo-size-sm);
color: var(--color-text-inverse);
}
.current {
font-weight: var(--typo-weight-bold);
color: var(--color-primary);
}
.link {
color: var(--color-text-inverse);
transition: color 0.5s ease-in-out;
&:hover {
color: var(--color-tertiary);
}
}
.publicationDate {
font-family: var(--font-mono), monospace;
font-size: var(--typo-size-xs);
font-weight: var(--typo-weight-bold);
color: var(--color-text-inverse);
}

View File

@@ -0,0 +1,48 @@
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}>
<div className={styles.wrapper}>
<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>
</div>
);
}

View File

@@ -0,0 +1,13 @@
.wrapper {
@mixin py var(--spacing-cozy);
}
.title {
font-family: var(--el-h1-font-family), serif;
font-size: var(--el-h1-font-size);
font-weight: var(--el-h1-font-weight);
line-height: var(--el-h1-line-height);
color: var(--el-h1-color);
text-transform: var(--el-h1-text-transform);
letter-spacing: var(--el-h1-letter-spacing);
}

View File

@@ -0,0 +1,13 @@
import styles from './Title.module.css';
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,56 @@
.item {
& .summary {
--el-summary-marker-symbol-closed: "▾";
--el-summary-marker-symbol-open: "▾";
--el-summary-marker-symbol-transform-open: rotate(180deg);
position: relative;
padding: var(--spacing-snug);
font-family: var(--el-summary-font-family);
font-size: var(--el-summary-font-size);
font-weight: var(--el-summary-font-weight);
line-height: var(--el-summary-line-height);
color: var(--el-summary-fg);
text-transform: var(--el-summary-text-transform);
list-style: none;
background: var(--el-summary-bg);
&::before {
content: var(--el-summary-marker-symbol-closed);
position: absolute;
right: 0;
font-size: 1em;
transition: transform 0.2s ease;
}
}
& .content {
overflow: hidden;
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 2s ease;
& .inner {
min-height: 0;
}
}
&[open] {
& .summary {
&::before {
content: var(--el-summary-marker-symbol-open);
transform: var(--el-summary-marker-symbol-transform-open);
}
}
& .content {
grid-template-rows: 1fr;
}
}
}

View File

@@ -0,0 +1,39 @@
'use client';
import React from 'react';
import styles from './AccordionItem.module.css';
interface AccordionItemProps {
title: string;
defaultOpen: boolean;
children: React.ReactNode;
}
export default function AccordionItem({
title,
defaultOpen,
children,
}: AccordionItemProps) {
const [isOpen, setIsOpen] = React.useState(defaultOpen);
return (
<details
className={styles.item}
open={isOpen}
onToggle={(e) => setIsOpen(e.currentTarget.open)}
>
<summary
className={styles.summary}
onClick={(e) => {
e.preventDefault();
setIsOpen(!isOpen);
}}
>
{title}
</summary>
<div className={styles.content}>
<div className={styles.inner}>{children}</div>
</div>
</details>
);
}

View File

@@ -0,0 +1,11 @@
import React from 'react';
import styles from './Accordion.module.css';
interface AccordionProps {
children: React.ReactNode;
}
export default function Accordion({ children }: AccordionProps) {
return <div className={styles.accordion}>{children}</div>;
}

View File

@@ -0,0 +1,27 @@
.container {
margin-block: var(--el-p-vspace-top) var(--el-p-vspace-bottom);
padding: var(--spacing-snug) 0 var(--spacing-snug) var(--spacing-comfortable);
border-left: var(--size-4) solid var(--color-text-tertiary);
}
.quote {
font-family: var(--el-blockquote-font-family), serif;
font-size: var(--el-blockquote-font-size);
font-weight: var(--el-blockquote-font-weight);
font-style: var(--el-blockquote-font-style);
line-height: var(--el-blockquote-line-height);
color: var(--el-blockquote-color);
}
.attribution,
.source {
font-family: var(--font-mono);
}
.link {
@mixin anim-txt-typewriter;
&:hover {
color: var(--color-primary);
}
}

View File

@@ -0,0 +1,70 @@
import { marked } from 'marked';
import styles from './Blockquote.module.css';
interface BlockquoteProps {
quote: string;
attribution?: string;
source?: string;
url?: string;
}
export default function Blockquote({
quote,
attribution,
source,
url,
}: BlockquoteProps) {
const hasAttribution = !!attribution;
const hasSource = !!source;
const hasUrl = !!url;
const showFooter = hasSource || hasAttribution;
const attributionLinked = hasAttribution && hasUrl && !hasSource;
const sourceLinked = hasSource && hasUrl;
return (
<blockquote className={styles.blockquote}>
<div
className={styles.quote}
dangerouslySetInnerHTML={{ __html: marked.parse(quote) }}
/>
{showFooter && (
<footer className={styles.footer}>
{hasAttribution && (
<cite className={styles.attribution}>
{attributionLinked ? (
<a
href={url}
className={styles.link}
target="_blank"
rel="noopener noreferrer"
>
{attribution}
</a>
) : (
attribution
)}
</cite>
)}
{hasSource && (
<cite className={styles.source}>
{' '}
{sourceLinked ? (
<a
href={url}
className={styles.link}
target="_blank"
rel="noopener noreferrer"
>
{source}
</a>
) : (
source
)}
</cite>
)}
</footer>
)}
</blockquote>
);
}

View File

@@ -0,0 +1,144 @@
@layer component {
.container {
--callout-bg: var(--color-surface-inverse);
--callout-fg: var(--color-text-inverse);
--callout-symbol: "";
--callout-symbol-color: var(--color-text-inverse);
@mixin my var(--spacing-cozy);
position: relative;
padding: var(--spacing-cozy) var(--spacing-cozy) var(--spacing-cozy) var(--size-12) ;
border: var(--size-1) solid var(--color-surface-inverse);
box-shadow: 2px 2px 0 oklch(from var(--color-surface-inverse) calc(l - 0.075) c h), 4px 4px 0 oklch(from var(--color-surface-inverse) calc(l - 0.2) c h);
&::before {
@mixin pt 0.425em;
content: var(--callout-symbol);
position: absolute;
top: 0;
left: 0;
display: flex;
align-items: flex-start;
justify-content: center;
width: var(--size-8);
height: 100%;
font-size: 1.5em;
color: var(--callout-symbol-color);
background-color: var(--color-surface-inverse);
}
}
.title {
@mixin text-xl;
font-family: var(--font-header);
font-weight: var(--typo-weight-black);
}
.badge {
position: absolute;
top: 0;
right: 0;
padding: 0.2em 0.5em;
font-family: var(--font-mono);
font-size: var(--typo-size-xs);
font-weight: var(--typo-weight-black);
color: var(--callout-fg);
background: var(--callout-bg);
}
.example {
--callout-bg: var(--color-palette-fuchsia);
--callout-symbol: "◆";
}
.info {
--callout-bg: var(--color-state-info);
--callout-symbol: "‽";
}
.warning {
--callout-bg: var(--color-state-warning);
--callout-symbol: "‼";
}
.tip {
--callout-bg: var(--color-palette-lime-green);
--callout-symbol: "★";
}
.spoiler {
& .label {
@mixin text-xl;
font-family: var(--font-header);
font-weight: var(--typo-weight-black);
&::before {
content: "[REDACTED] ";
color: var(--color-state-error);
}
&:hover {
cursor: pointer;
}
}
& .content {
pointer-events: none;
position: relative;
min-height: 3em;
&::after {
content: '████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████';
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
overflow: hidden;
font-family: var(--font-mono);
line-height: inherit;
color: var(--color-text-primary);
letter-spacing: -0.15em;
word-break: break-all;
white-space: pre-wrap;
background: var(--color-text-primary);
}
}
& .toggle {
@util hide-visually;
&:checked {
& ~ .label {
&::before {
content: '[REVEALED] ';
color: var(--color-state-success);
}
}
& ~ .content {
&::after {
content: none;
}
}
}
}
}
}

View File

@@ -0,0 +1,32 @@
import React from 'react';
import styles from './Callout.module.css';
interface CalloutProps {
type: 'default' | 'example' | 'info' | 'warning' | 'tip' | 'spoiler';
title?: string;
children: React.ReactNode;
}
export default function Callout({ type, title, children }: CalloutProps) {
const isSpoiler = type === 'spoiler';
const spoilerID = isSpoiler
? `spoiler-${Math.random().toString(36).substr(2, 9)}`
: undefined;
return (
<div className={`${styles.container} ${styles[type]}`}>
{isSpoiler ? (
<>
<input type="checkbox" id={spoilerID} className={styles.toggle} />
<label htmlFor={spoilerID} className={styles.label}>
{title || 'Show spoiler'}
</label>
</>
) : (
<p className={styles.title}>{title}</p>
)}
<span className={styles.badge}>{type.toUpperCase()}</span>
<div className={`${styles.content}`}>{children}</div>
</div>
);
}

View File

@@ -0,0 +1,24 @@
.term {
margin-block: var(--el-dt-vspace-top) var(--el-dt-vspace-bottom);
padding: var(--el-dt-padding);
font-family: var(--el-dt-font-family), serif;
font-size: var(--el-dt-font-size);
font-weight: var(--el-dt-font-weight);
line-height: var(--el-dt-line-height);
color: var(--el-dt-color);
text-transform: var(--el-dt-text-transform);
letter-spacing: var(--el-dt-letter-spacing);
background: var(--el-dt-background);
}
.def {
margin-block: var(--el-dd-vspace-top) var(--el-dd-vspace-bottom);
padding: var(--el-dd-indent);
font-family: var(--el-dd-font-family), sans-serif;
font-size: var(--el-dd-font-size);
line-height: var(--el-dd-line-height);
color: var(--el-dd-color);
}

View File

@@ -0,0 +1,28 @@
import { marked } from 'marked';
import styles from './DefinitionItem.module.css';
interface DefinitionItemProps {
term: string;
definitions: string[];
}
export default function DefinitionItem({
term,
definitions,
}: DefinitionItemProps) {
return (
<>
<dt className={styles.term}>{term}</dt>
{definitions.map((def, idx) => (
<dd
key={idx}
className={styles.def}
dangerouslySetInnerHTML={{
__html: marked.parseInline(def),
}}
/>
))}
</>
);
}

View File

@@ -0,0 +1,5 @@
.list {
margin-block: var(--el-dl-vspace-top) var(--el-dl-vspace-bottom);
font-size: var(--el-dl-font-size);
line-height: var(--el-dl-line-height);
}

View File

@@ -0,0 +1,10 @@
import React from 'react';
import styles from './DefinitionList.module.css';
interface DefinitionListProps {
children?: React.ReactNode;
}
export default function DefinitionList({ children }: DefinitionListProps) {
return <dl className={styles.list}>{children}</dl>;
}

View File

@@ -0,0 +1,29 @@
.figure {
position: relative;
border: var(--size-2) solid var(--color-surface-inverse);
clip-path: polygon(0 var(--size-12), var(--size-12) 0, 100% 0, 100% 100%, 0 100%);
}
.caption {
padding: var(--spacing-snug);
font-family: var(--font-mono);
font-size: var(--typo-size-sm);
line-height: var(--typo-leading-relaxed);
color: var(--color-text-inverse);
background: var(--color-surface-inverse);
}
.credit {
display: block;
margin-top: var(--spacing-tight);
font-family: var(--font-mono);
font-size: var(--typo-size-xs);
font-style: normal;
color: var(--color-text-inverse);
text-align: right;
}

View File

@@ -0,0 +1,34 @@
import Image from 'next/image';
import styles from './Figure.module.css';
interface FigureProps {
src: string;
alt: string;
caption?: string;
credit?: string;
}
export default function Figure({ src, alt, caption, credit }: FigureProps) {
const hasCaption = !!caption;
const hasCredit = !!credit;
const showFigcaption = hasCaption || hasCredit;
return (
<figure className={styles.figure}>
<Image
src={src}
alt={alt}
width={800}
height={600}
className={styles.image}
/>
{showFigcaption && (
<figcaption className={styles.caption}>
{hasCaption && <span className={styles.text}>{caption}</span>}
{hasCredit && <cite className={styles.credit}>[{credit}]</cite>}
</figcaption>
)}
</figure>
);
}

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 default 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 default function Row({ style, children }: RowProps) {
return (
<div
className={`${styles.row}`}
style={{
gridTemplateColumns: `repeat(${style.cols}, 1fr)`,
gap: `${style.gap}`,
alignItems: `${style.align}`,
justifyContent: `${style.justify}`,
}}
>
{children}
</div>
);
}

View File

@@ -0,0 +1,69 @@
.container {
flex: 1 0 auto;
min-width: var(--size-96);
}
.wrapper {
@supports (anchor-name: --test) {
@media screen and (--bp-margin) {
position: absolute;
top: anchor(top);
}
}
}
.note {
@mixin text-xs;
@mixin my var(--spacing-cozy);
position: relative;
padding-left: var(--size-12);
font-family: var(--font-mono);
@supports (anchor-name: --test) {
@media screen and (--bp-margin) {
min-width: var(--size-64);
margin-top: 0;
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 {
display: contents;
}
.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,41 @@
import React from 'react';
import { marked } from 'marked';
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}` } as React.CSSProperties
}
>
<div className={`${styles.note} ${styles[sidenote.type]}`}>
<span className={styles.marker}>
<span className={styles.symbol}>{sidenote.marker}</span>
</span>
<div
className={`${styles.content} content`}
dangerouslySetInnerHTML={{
__html: marked.parseInline(sidenote.content),
}}
/>
<a href={`#ref-${sidenote.id}`} className={styles.ref}>
</a>
</div>
</div>
))}
</aside>
);
}

View File

@@ -0,0 +1,16 @@
.ref {
@mixin px var(--spacing-tight);
scroll-margin-top: calc(var(--el-header-height) + var(--spacing-comfortable));
display: inline-block;
font-weight: var(--typo-weight-black);
color: var(--color-secondary);
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}` } as React.CSSProperties}
>
[{marker}]
</a>
</sup>
);
}

View File

@@ -0,0 +1,25 @@
import Row from '@/components/Content/Grid/Row';
import Column from '@/components/Content/Grid/Column';
import Sidenote from '@/components/Content/Sidenote/Item';
import DefinitionList from '@/components/Content/DefinitionList';
import DefinitionItem from '@/components/Content/DefinitionList/DefinitionItem';
import Accordion from '@/components/Content/Accordion';
import AccordionItem from '@/components/Content/Accordion/AccordionItem';
import Blockquote from '@/components/Content/Blockquote';
import Figure from '@/components/Content/Figure';
import Callout from '@/components/Content/Callout';
const ContentComponents = {
Column,
Row,
Sidenote,
DefinitionList,
DefinitionItem,
Accordion,
AccordionItem,
Blockquote,
Figure,
Callout,
};
export default ContentComponents;

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,14 @@
import { collection } from '@keystatic/core';
import { createArticleField } from '@/keystatic/fields/article';
export default collection({
label: 'AWQ - Posts',
slugField: 'title',
path: 'content/awq/articles/*',
format: { contentField: 'content' },
entryLayout: 'content',
schema: {
...createArticleField('awq/articles', 'awq'),
},
});

View File

@@ -0,0 +1,7 @@
import posts from '@/keystatic/collections/awq/article';
const awqCollections = {
awq_posts: posts,
};
export default awqCollections;

View File

@@ -0,0 +1,14 @@
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' },
entryLayout: '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,32 @@
import { listCollapseIcon } from '@keystar/ui/icon/icons/listCollapseIcon';
import { repeating, wrapper } from '@keystatic/core/content-components';
import { fields } from '@keystatic/core';
const accordionComponents = {
Accordion: repeating({
label: 'Accordion',
icon: listCollapseIcon,
children: ['AccordionItem'],
schema: {},
}),
AccordionItem: wrapper({
label: 'Accordion Item',
forSpecificLocations: true,
schema: {
title: fields.text({
label: 'Title',
validation: {
length: {
min: 1,
},
},
}),
defaultOpen: fields.checkbox({
label: 'Open by default',
defaultValue: false,
}),
},
}),
};
export default accordionComponents;

View File

@@ -0,0 +1,34 @@
import { textQuoteIcon } from '@keystar/ui/icon/icons/textQuoteIcon';
import { block } from '@keystatic/core/content-components';
import { fields } from '@keystatic/core';
const blockquoteComponent = {
Blockquote: block({
label: 'Blockquote',
icon: textQuoteIcon,
schema: {
quote: fields.text({
label: 'Quote',
multiline: true,
validation: {
length: {
min: 1,
},
},
}),
attribution: fields.text({
label: 'Attribution',
description: 'Author or Speaker',
}),
source: fields.text({
label: 'Source',
description: 'Book, article, or document [optional]',
}),
url: fields.url({
label: 'Source URL',
}),
},
}),
};
export default blockquoteComponent;

View File

@@ -0,0 +1,47 @@
import { wrapper } from '@keystatic/core/content-components';
import { fields } from '@keystatic/core';
import { megaphoneIcon } from '@keystar/ui/icon/icons/megaphoneIcon';
const calloutComponent = {
Callout: wrapper({
label: 'Callout',
icon: megaphoneIcon,
schema: {
type: fields.select({
label: 'Type',
defaultValue: 'default',
options: [
{
label: 'Default',
value: 'default',
},
{
label: 'Example',
value: 'example',
},
{
label: 'Info',
value: 'info',
},
{
label: 'Warning',
value: 'warning',
},
{
label: 'Tip',
value: 'tip',
},
{
label: 'Spoiler',
value: 'spoiler',
},
],
}),
title: fields.text({
label: 'Title',
}),
},
}),
};
export default calloutComponent;

View File

@@ -0,0 +1,28 @@
import { listTreeIcon } from '@keystar/ui/icon/icons/listTreeIcon';
import { fields } from '@keystatic/core';
import { repeating, block } from '@keystatic/core/content-components';
const definitionlistComponents = {
DefinitionList: repeating({
label: 'Definition List',
icon: listTreeIcon,
children: ['DefinitionItem'],
schema: {},
}),
DefinitionItem: block({
label: 'Definition Item',
forSpecificLocations: true,
schema: {
term: fields.text({ label: 'Term' }),
definitions: fields.array(
fields.text({ label: 'Definition', multiline: true }),
{
label: 'Definitions',
itemLabel: (props) => props.value || 'Definition',
}
),
},
}),
};
export default definitionlistComponents;

View File

@@ -0,0 +1,35 @@
import { imageIcon } from '@keystar/ui/icon/icons/imageIcon';
import { block } from '@keystatic/core/content-components';
import { fields } from '@keystatic/core';
const figureComponent = {
Figure: block({
label: 'Figure',
icon: imageIcon,
schema: {
src: fields.image({
label: 'Image',
directory: 'public/images/figures',
publicPath: '/images/figures',
}),
alt: fields.text({
label: 'Alt Text',
validation: {
length: {
min: 1,
},
},
}),
caption: fields.text({
label: 'Caption',
multiline: true,
}),
credit: fields.text({
label: 'Credit/Attribution',
description: 'Photographer, artist, or source',
}),
},
}),
};
export default figureComponent;

View File

@@ -0,0 +1,62 @@
import { grid2X2Icon } from '@keystar/ui/icon/icons/grid2X2Icon';
import { fields } from '@keystatic/core';
import { repeating, wrapper } from '@keystatic/core/content-components';
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,
}),
}),
},
}),
};
export default gridComponents;

View File

@@ -0,0 +1,17 @@
import gridComponents from '@/keystatic/components/general/grid';
import sidenoteComponents from '@/keystatic/components/general/sidenote';
import definitionlistComponents from '@/keystatic/components/general/definitionlist';
import accordionComponents from '@/keystatic/components/general/accordion';
import blockquoteComponent from '@/keystatic/components/general/blockquote';
import figureComponent from '@/keystatic/components/general/figure';
import calloutComponent from '@/keystatic/components/general/callout';
export const generalComponents = {
...gridComponents,
...sidenoteComponents,
...definitionlistComponents,
...accordionComponents,
...blockquoteComponent,
...figureComponent,
...calloutComponent,
};

View File

@@ -0,0 +1,61 @@
import { panelRightDashedIcon } from '@keystar/ui/icon/icons/panelRightDashedIcon';
import { fields } from '@keystatic/core';
import { inline } from '@keystatic/core/content-components';
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',
}),
},
}),
};
export default sidenoteComponents;

View File

@@ -0,0 +1,31 @@
import { fields } from '@keystatic/core';
import { createContentField } from '@/keystatic/fields/content';
import { createPathField } from '@/keystatic/fields/general';
import { createSEOField } from '@/keystatic/fields/seo';
import { createMetaField } from '@/keystatic/fields/meta';
export const createArticleField = (
imageSubfolder: string,
defaultPath: string = ''
) => ({
title: fields.slug({ name: { label: 'Title' } }),
summary: fields.text({ label: 'Summary', multiline: true }),
path: createPathField(defaultPath),
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,304 @@
import { type ComponentSchema, fields } from '@keystatic/core';
import { createOperatorField } from '@/keystatic/fields/general';
export const createCharacteristicReferenceField = (): ComponentSchema =>
fields.select({
label: 'Characteristic',
options: [
{ label: 'Weapon Skill', value: 'ws' },
{ label: 'Ballistic Skill', value: 'bs' },
{ label: 'Strength', value: 's' },
{ label: 'Toughness', value: 't' },
{ label: 'Initiative', value: 'i' },
{ label: 'Dexterity', value: 'dex' },
{ label: 'Agility', value: 'ag' },
{ label: 'Intelligence', value: 'int' },
{ label: 'Willpower', value: 'wp' },
{ label: 'Fellowship', value: 'fel' },
],
defaultValue: 'ws',
});
export const createSkillReferenceField = (): ComponentSchema =>
fields.select({
label: 'Skill',
options: [
{
label: 'Melee',
value: 'melee',
},
{
label: 'Defence',
value: 'defence',
},
{
label: 'Shooting',
value: 'shooting',
},
{
label: 'Throwing',
value: 'throwing',
},
{
label: 'Brawn',
value: 'brawn',
},
{
label: 'Toil',
value: 'toil',
},
{
label: 'Consume Alcohol',
value: 'consume-alcohol',
},
{
label: 'Endurance',
value: 'endurance',
},
{
label: 'Perception',
value: 'perception',
},
{
label: 'Dodge',
value: 'dodge',
},
{
label: 'Skullduggery',
value: 'skullduggery',
},
{
label: 'Tradecraft',
value: 'tradecraft',
},
{
label: 'Stealth',
value: 'stealth',
},
{
label: 'Athletics',
value: 'athletics',
},
{
label: 'Intuition',
value: 'intuition',
},
{
label: 'Education',
value: 'education',
},
{
label: 'Discipline',
value: 'discipline',
},
{
label: 'Animal Handling',
value: 'animal-handling',
},
{
label: 'Leadership',
value: 'leadership',
},
{
label: 'Charm',
value: 'charm',
},
],
defaultValue: 'melee',
});
export const createSizeReferenceField = (): ComponentSchema =>
fields.select({
label: 'Size',
options: [
{
label: 'Tiny',
value: 'tiny',
},
{
label: 'Little',
value: 'little',
},
{
label: 'Small',
value: 'small',
},
{
label: 'Average',
value: 'average',
},
{
label: 'Large',
value: 'large',
},
{
label: 'Enormous',
value: 'enormous',
},
{
label: 'Monstrous',
value: 'monstrous',
},
],
defaultValue: 'average',
});
export const createPsychologyReferenceField = (): ComponentSchema =>
fields.object({
type: fields.select({
label: 'Psychology Type',
defaultValue: 'animosity',
options: [
{ label: 'Animosity', value: 'animosity' },
{ label: 'Fear', value: 'fear' },
{ label: 'Frenzy', value: 'frenzy' },
{ label: 'Hatred', value: 'hatred' },
{ label: 'Prejudice', value: 'prejudice' },
{ label: 'Terror', value: 'terror' },
],
}),
target: fields.text({
label: 'Target',
description: 'e.g. Elves, Undead, Chaos',
}),
rating: fields.number({
label: 'Rating',
description: 'Fear/Terror Only',
}),
});
export const createRequirementsField = () =>
fields.blocks(
{
characteristic: {
label: 'Characteristic',
schema: fields.object({
characteristic: createCharacteristicReferenceField(),
operator: createOperatorField(),
value: fields.number({ label: 'Value', defaultValue: 3 }),
}),
},
skill: {
label: 'Skill',
schema: fields.object({
skill: createSkillReferenceField(),
operator: createOperatorField(),
value: fields.number({ label: 'Value', defaultValue: 3 }),
}),
},
status: {
label: 'Status',
schema: fields.multiselect({
label: 'Status Tier',
options: [
{
value: 'copper',
label: 'Copper',
},
{
value: 'silver',
label: 'Silver',
},
{
value: 'gold',
label: 'gold',
},
],
defaultValue: ['copper'],
}),
},
talent: {
label: 'Talent',
schema: fields.relationship({
label: 'Talent',
collection: 'awq_talents',
}),
},
lore: {
label: 'Lore',
schema: fields.relationship({
label: 'Lore',
collection: 'awq_lores',
}),
},
},
{ label: 'Requirements' }
);
export const createTraitsField = (): ComponentSchema =>
fields.array(
fields.object({
name: fields.text({
label: 'Trait name',
}),
description: fields.text({
label: 'Description',
multiline: true,
}),
grants: fields.blocks(
{
talent: {
label: 'Talent',
schema: fields.relationship({
label: 'Talent',
collection: 'awq_talents',
}),
},
talent_options: {
label: 'Talent Options',
schema: fields.object({
choices: fields.number({
label: 'Number to choose',
defaultValue: 1,
}),
options: fields.array(
fields.relationship({
label: 'Talent Options',
collection: 'awq_talents',
})
),
}),
},
talent_random: {
label: 'Random Talent',
schema: fields.object({
rolls: fields.number({
label: 'Rolls',
defaultValue: 1,
}),
table: fields.array(
fields.object({
roll: fields.object({
min: fields.number({ label: 'Min' }),
max: fields.number({ label: 'Max' }),
}),
talent: fields.relationship({
label: 'Talent',
collection: 'awq_talents',
}),
})
),
}),
},
psychology: {
label: 'Psychology',
schema: createPsychologyReferenceField(),
},
custom: {
label: 'Custom Effect',
schema: fields.text({
label: 'Custom Effect',
multiline: true,
}),
},
},
{
label: 'Trait Grants',
}
),
}),
{
label: 'Traits',
itemLabel: (props) => props.fields.name.value || 'Unnamed Trait',
}
);

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,21 @@
import { ComponentSchema, fields } from '@keystatic/core';
export const createOperatorField = (): ComponentSchema =>
fields.select({
label: 'Operator',
options: [
{ label: '>=', value: 'gte' },
{ label: '>', value: 'gt' },
{ label: '=', value: 'eq' },
{ label: '<', value: 'lt' },
{ label: '<=', value: 'lte' },
],
defaultValue: 'eq',
});
export const createPathField = (defaultValue: string): ComponentSchema =>
fields.text({
label: 'Path',
description: 'Path on the website',
defaultValue: defaultValue,
});

View File

@@ -0,0 +1,43 @@
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',
}
);

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: '',
},
};

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

@@ -0,0 +1,127 @@
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',
},
},
},
DefinitionList: {
render: 'DefinitionList',
children: ['tag'],
},
DefinitionItem: {
render: 'DefinitionItem',
selfClosing: false,
attributes: {
term: {
type: 'String',
required: true,
},
definitions: {
type: 'Array',
required: true,
},
},
},
Accordion: {
render: 'Accordion',
children: ['tag'],
},
AccordionItem: {
render: 'AccordionItem',
attributes: {
title: {
type: 'String',
required: true,
},
defaultOpen: {
type: 'Boolean',
default: false,
},
},
},
Blockquote: {
render: 'Blockquote',
attributes: {
quote: {
type: 'String',
required: true,
},
attribution: {
type: 'String',
},
source: {
type: 'String',
},
url: {
type: 'String',
},
},
},
Figure: {
render: 'Figure',
attributes: {
src: {
type: 'String',
required: true,
},
alt: {
type: 'String',
required: true,
},
caption: {
type: 'String',
},
credit: {
type: 'String',
},
},
},
Callout: {
render: 'Callout',
attributes: {
type: {
type: 'String',
default: 'default',
},
title: {
type: 'String',
},
},
},
};

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,11 @@
export interface Sidenote {
id: string;
marker: string;
content: string;
type: 'default' | 'lore' | 'crunch' | 'example';
}
export interface DefinitionItem {
term: string;
definition: string[];
}

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;
}

Some files were not shown because too many files have changed in this diff Show More