From 299084de93ef8efa828d3fd082be898a8a8091dd Mon Sep 17 00:00:00 2001
From: Matthias Kretschmann
Date: Mon, 18 Sep 2023 02:16:53 +0100
Subject: [PATCH] =?UTF-8?q?Gatsby=20=E2=86=92=20Astro=20(#829)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* basic astro setup, kick out all gatsby configs
* move content folder
* src/pages setup
* more file reorg
* more config updates
* more reorgs
* refactor
* refactor
* bump astro
* refactor
* svg icon build system, theme switch
* remark plugin for lead paragraph, more refactor
* make images work
* post meta
* custom Picture component
* Pagination, More component, 404 fixes
* linking fixes
* add table of contents
* post actions fixes
* tag fixes
* content changes
* content changes: move media files to their posts
* more content moving, remove media folder
* refactor remark-lead-paragraph
* link css file when defined in post frontmatter
* move content up again
* kbd post update
* allow js
* downloads solution
* add astro check
* redirect_from solution
* githubLink solution
* reorg
* exif solution as prebuild step
* exif solution on each post during build
* isolate lead paragraph extraction to articles
* restore Exif components
* deploy script update
* fix redirects
* xml & json feeds
* build fix
* fix exif readout in production
* head and seo tweaks, add feeds
* tweak image display
* archive pages with single layout
* restore tags archive
* sitemap setup
* restore thanks page functionality
* reorg
* cleanup
* parallel scripts during prebuild
* restore jest setup
* remove jest, switch to vitest as test runner
* adapt CI
* test refactor
* typescript tweaks
* avatar fixes
* typings
* restore search functionality
* theme switch with nanostores
* fixes
* test fixes
* restore changelog functionality
* umami script
* border color tweak
* related posts with fuse.js
* plawright e2e testing setup
* search tweaks
* simplify typekit loading
* photo fix
* e2e tests
* related posts fix
* fix tags archive
* tweaks
* tweaks
* linux snapshots
* fix header test
* new symlink tactic
* fix dev server in codespaces
* fix yaml
* ci fixes
* changelog loading tweaks
* e2e against dev server on ci
* changelog tweaks
* ci tweaks
* ci tweaks
* ci tweaks
* docs updates
* ci tweaks
* refactor photo creation script
* package updates
* refactor search
* ci tweak
* ci tweaks
* test tweaks, more unit tests
* more unit tests
* post creation script tweaks
* refactor new scripts, test them for real life
* more tests
* refactor
* codeclimate-action update
* uses update
* limit ci runs
* fix theme toggle, test it
* more tests
* favicon files cleanup
* icon components location change
* type checking through ci
* command fixes
* ci fix
* search tweaks
* ci tweaks
* revised favicons, write post draft about it
* drafts filtering fix
* lint fix, favicon fixes
* copy changes
* fix related search images
* content updates
* new codeblock styles, copy tweaks, fixes
* package updates
* typing fixes
* lint fix
* content updates
* restore link posts
* faster theme switching
* split up astro utils
* related posts fixes
* fix
* refactor
* fixes
* copy tweaks
* fixes
* picture tweaks
* image fixes
* feed fixes, adapt for json feed v1.1
* e2e test updates
* layout tweaks
* update snaphots
* migrate to createMarkdownProcessor
* ci cache tweaks
* activate more browsers for e2e testing
* switch to macos-13 images
* build caching tweaks
* markdown fix
* set image quality
* remove avif generation
* picture tweaks
* head fixes
* add og:image:alt
* create-icons test
* new post: Favicon Generation with Astro
---
.../.markdownlint.json | 0
.stylelintrc => .config/.stylelintrc.json | 0
.config/astro.config.ts | 55 +
.config/aws_redirects.xml | 74 +
config.ts => .config/blog.config.ts | 10 +-
.env.sample | 12 +-
.eslintrc => .eslintrc.json | 33 +-
.github/workflows/ci.yml | 149 +-
.github/workflows/codeql-analysis.yml | 2 +-
.gitignore | 22 +-
.jest/__fixtures__/avatar.json | 22 -
.jest/__fixtures__/github.json | 219 -
.jest/__fixtures__/home.json | 523 -
.jest/__fixtures__/link.json | 25 -
.jest/__fixtures__/meta.json | 43 -
.jest/__fixtures__/photos.json | 62 -
.jest/__fixtures__/post.json | 41 -
.jest/__fixtures__/postWithMore.json | 41 -
.jest/__fixtures__/posts.json | 356 -
.jest/__mocks__/file.js | 1 -
.jest/__mocks__/gatsby.js | 29 -
.jest/__mocks__/matchMedia.ts | 27 -
.jest/__mocks__/svgr.js | 6 -
.jest/__mocks__/wagmi.js | 72 -
.jest/babel.config.js | 4 -
.jest/jest.config.js | 58 -
.jest/loadershim.js | 3 -
.jest/setup-test-env.ts | 23 -
.jest/testRender.ts | 12 -
.prettierrc | 18 -
.prettierrc.json | 14 +
README.md | 161 +-
_aws/redirects.xml | 70 -
content/_schemas.ts | 71 +
.../index.md} | 0
.../canon1.png | Bin
.../canon2.png | Bin
.../canon3.png | Bin
.../canon4.png | Bin
.../canoscanlide500f.jpg | Bin
.../index.md} | 10 +-
.../index.md} | 0
.../index.md} | 0
.../index.md} | 4 +-
.../parallax.png | Bin
.../Teaser-Aperture-File-Types.jpg | Bin
.../aperturefiletypes_by_kremalicious.zip | Bin
.../index.md} | 6 +-
.../index.md} | 4 +-
.../pulitzer.png | Bin
.../PS_CR2FileIcon.png | Bin
.../aperturefiletypes.png | Bin
.../index.md} | 22 +-
.../openwithpreview.png | Bin
.../pathphotoshop.png | Bin
.../pathpreview.png | Bin
.../raw.png | Bin
.../apassis.png | Bin
.../index.md} | 6 +-
.../index.md} | 6 +-
.../webkit.png | Bin
.../css.png | Bin
.../index.md} | 48 +-
.../navbar-kremalicious.png | Bin
.../text-shadow-1.png | Bin
.../text-shadow-10.png | Bin
.../text-shadow-11.png | Bin
.../text-shadow-12.png | Bin
.../text-shadow-13.png | Bin
.../text-shadow-14.png | Bin
.../text-shadow-15.png | Bin
.../text-shadow-2.png | Bin
.../text-shadow-3.png | Bin
.../text-shadow-4.png | Bin
.../text-shadow-5.png | Bin
.../text-shadow-6.png | Bin
.../text-shadow-7.png | Bin
.../text-shadow-8.png | Bin
.../text-shadow-9.png | Bin
.../text-shadow-hack.png | Bin
.../textshadow-apple.png | Bin
...4-22-apple-releases-sdk-for-aperture-21.md | 17 -
.../aperture97.png | Bin
.../index.md | 19 +
.../index.md} | 4 +-
.../webkit.png | Bin 0 -> 36541 bytes
.../aperturescan.png | Bin
.../canondrivercontents.png | Bin
.../canoscanlide500f.jpg | Bin 0 -> 58689 bytes
.../index.md} | 14 +-
.../scangear16bit.png | Bin
.../scangear48bit.png | Bin
.../scangear_color.png | Bin
.../scangearwindow.png | Bin
.../scanwindow_preview.png | Bin
...can-images-directly-into-apple-aperture.md | 38 -
.../apertureimport_automatic.png | Bin
.../apertureimport_automator.png | Bin
.../apertureimportplugin.png | Bin
.../aperturescan.png | Bin 0 -> 30914 bytes
.../automator.png | Bin
.../index.md | 42 +
.../index.md} | 34 +-
.../watermark_1.png | Bin
.../watermark_2.png | Bin
.../watermark_3.png | Bin
.../watermark_4.png | Bin
.../watermark_5.png | Bin
.../watermark_6.png | Bin
.../watermark_7.png | Bin
.../watermark_8.png | Bin
.../watermark_9.png | Bin
.../watermark_aperture.jpg | Bin
.../watermark_example_by_kremalicious.zip | Bin
.../watermarkexample_final.jpg | Bin
.../watermarkexample_v2.jpg | Bin
.../watermarkexample_v3.jpg | Bin
.../watermarkexample_v4.jpg | Bin
.../index.md} | 4 +-
.../viveza.png | Bin
.../aperture15_1.png | Bin
.../aperture15_10.png | Bin
.../aperture15_11.png | Bin
.../aperture15_12.png | Bin
.../aperture15_13.png | Bin
.../aperture15_14.jpg | Bin
.../aperture15_15.png | Bin
.../aperture15_2.png | Bin
.../aperture15_3.png | Bin
.../aperture15_4.png | Bin
.../aperture15_5.png | Bin
.../aperture15_6.png | Bin
.../aperture15_7.png | Bin
.../aperture15_8.jpg | Bin
.../aperture15_8.png | Bin
.../index.md} | 34 +-
.../raw20.png | Bin
.../index.md} | 6 +-
.../indianajones_dock.png | Bin
.../indianajones_first.png | Bin
.../index.md} | 0
.../aperture97.png | Bin 0 -> 17737 bytes
.../apertureedit_interface.jpg | Bin
.../creaceed_hydra.png | Bin
.../dft-light_ui.jpg | Bin
.../dft-ozone_ui.jpg | Bin
.../dft-powerstroke_uis.jpg | Bin
.../dpmatte_index1.jpg | Bin
.../imagestrends_shieoff.jpg | Bin
.../imagetrends_hemi_10.jpg | Bin
.../imagetrends_pearlywhite.png | Bin
.../index.md} | 32 +-
.../lensfix_ci.png | Bin
.../noiseninja.png | Bin
.../tiffen_aperture.jpg | Bin
.../viveza_aperture_ui.jpg | Bin
.../imagetrends_hemi_10.jpg | Bin 0 -> 167841 bytes
.../index.md} | 6 +-
.../index.md} | 6 +-
.../parallax_illusion_css.png | Bin
.../index.md} | 36 +-
.../parallax-visualization.png | Bin
.../parallax-visualization_big.png | Bin
.../parallax_001.jpg | Bin
.../parallax_002.jpg | Bin
.../parallax_003.jpg | Bin
.../parallax_004.jpg | Bin
.../parallax_005.jpg | Bin
.../parallax_006.jpg | Bin
.../parallax_007.jpg | Bin
.../parallax_008.jpg | Bin
.../parallax_009.jpg | Bin
.../parallax_010.jpg | Bin
.../parallax_011.jpg | Bin
.../parallax_012.jpg | Bin
.../parallax_013.jpg | Bin
.../parallax_014.png | Bin
.../parallax_015.png | Bin
.../parallax_016.jpg | Bin
.../parallax_017.jpg | Bin
.../apertureedit_logo.png | Bin
.../index.md} | 6 +-
content/articles/2008-06-01-chives.md | 23 -
.../2008-06-01-chives}/Teaser-Chives.jpg | Bin
.../chives_by_kremalicious.zip | Bin
content/articles/2008-06-01-chives/index.md | 23 +
.../index.md} | 4 +-
.../twitter-bird-kremalicious.png | Bin
.../twitter.png | Bin
.../Teaser-Camera-Obscura-Icons.jpg | Bin
.../View-from-the-Window-at-Le-Gras.png | Bin
.../cameraobscura11_all.png | Bin
.../cameraobscura_inuse.png | Bin
.../coffee-cup-empty.png | Bin
.../index.md} | 18 +-
.../nicephore-niepce.jpg | Bin
...niepces_camera_obscura_by_kremalicious.zip | Bin
...the_window_at_le_gras_nicephore_niepce.jpg | Bin
.../Teaser-Camera-Obscura-Walls.jpg | Bin
.../index.md} | 6 +-
...obscura_wallpaper_pack_by_kremalicious.zip | Bin
.../index.md} | 14 +-
.../safari-logo.png | Bin
.../safari4_zoom.png | Bin
.../safari_4_tabs.png | Bin
.../webinspector_2.png | Bin
.../webinspector_3.png | Bin
.../aperture128.png | Bin
.../aperture_bt.png | Bin
.../index.md} | 6 +-
.../bonjour97.png | Bin
.../coffee-cup-empty.png | Bin 0 -> 19496 bytes
.../index.md} | 52 +-
.../netatalk.png | Bin
.../server_displays_by_kremalicious.zip | Bin
.../serverdisplays.jpg | Bin
.../timemachine97.png | Bin
.../timemachinedisk97.png | Bin
.../tm-sparsebundle.png | Bin
.../ubuntu_mac_feature.jpg | Bin
.../ubuntuserver1.png | Bin
.../ubuntuserver2.png | Bin
.../ubuntuserver3.png | Bin
.../ubuntuserver4.png | Bin
.../ubuntuserver4a.png | Bin
.../ubuntuserver5.png | Bin
.../ubuntuserver97.png | Bin
.../apertureedit_logo2.png | Bin
.../index.md} | 6 +-
...25-new-canon-stuff-xs-eos-1000-430ex-ii.md | 17 -
.../canon1000d.png | Bin
.../index.md | 19 +
.../speedlite430exii.jpg | Bin
...ack-to-normal-on-kremaliciouscom-almost.md | 47 -
.../aperture-plugin128.png | Bin
.../index.md} | 8 +-
.../jade_ui.png | Bin
.../ptlens_ui.png | Bin
.../index.md} | 8 +-
.../kremalicious-iconiphone.png | Bin
.../kremaliciousiphone.png | Bin
...e-plug-in-nik-announces-silver-efex-pro.md | 19 -
.../aperture-plugin128.png | Bin 0 -> 18073 bytes
.../index.md | 21 +
.../nik_silverefex.png | Bin
.../index.md} | 8 +-
.../wordpress-logo.png | Bin
.../index.md} | 4 +-
.../jingjing_chacha_kremalicious.jpg | Bin
.../index.md} | 14 +-
.../niepces_aperture_vault256.png | Bin
.../securevault1.png | Bin
.../securevault2.png | Bin
.../securevault3.png | Bin
.../securevault4.png | Bin
.../securevault5.png | Bin
.../aperture-plugin128.png | Bin 0 -> 18073 bytes
.../index.md} | 8 +-
.../index.md} | 26 +-
.../marsedit_kremalicious.png | Bin
.../marsedit_kremalicious.txt | 0
.../marsedit_kremalicious_big.png | Bin
.../Teaser-Icy-Box.jpg | Bin
.../2008-08-26-icy-box-icons/icybox.jpg} | Bin
.../icybox_by_kremalicious.zip | Bin
.../index.md} | 8 +-
.../architect-icon.jpg | Bin
.../architect-ui.png | Bin
.../index.md} | 8 +-
.../magnifique-ui.png | Bin
...-08-28-canon-eos-50d-new-lens-announced.md | 27 -
.../efs_18-200.png | Bin
.../eos_50D_back.png | Bin
.../eos_50D_front.png | Bin
.../index.md | 31 +
.../chrome-ui.png | Bin
.../googlechrome.png | Bin
.../index.md} | 17 +-
.../Teaser-Mars-U.jpg | Bin
.../index.md} | 7 +-
.../mars-u-wall-by-kremalicious.zip | Bin
.../Teaser-Coffee-Cup-Icon.jpg | Bin
.../coffee_cup_by_kremalicious.zip | Bin
.../index.md} | 6 +-
.../Teaser-Coffee-Cup-Icon.jpg | Bin 0 -> 82023 bytes
.../coffee-showcase-apple.png | Bin
.../coffee-showcase-baumann.png | Bin
.../coffee-showcase-benedik.png | Bin
.../coffee-showcase-brasgalla.png | Bin
.../coffee-showcase-brasgalla2.png | Bin
.../coffee-showcase-cappuccinosofa.png | Bin
.../coffee-showcase-england.png | Bin
.../coffee-showcase-flarup.png | Bin
.../coffee-showcase-jaeppinen.png | Bin
.../coffee-showcase-kaycaffeine.png | Bin
.../coffee-showcase-kretschmann.png | Bin
.../coffee-showcase-lanham.png | Bin
.../coffee-showcase-lopezruiz.png | Bin
.../coffee-showcase-lovecappu.png | Bin
.../coffee-showcase-macrabbit.png | Bin
.../coffee-showcase-matu.png | Bin
.../coffee-showcase-rask1.png | Bin
.../coffee-showcase-rask2.png | Bin
.../coffee-showcase-susumo.png | Bin
.../coffee-showcase-times.png | Bin
.../coffee-showcase-tut-abduzeedo.png | Bin
.../coffee-showcase-tut-houle.png | Bin
.../coffee-showcase-tut-psdtuts.png | Bin
.../coffee-showcase-tut-vectuts.png | Bin
.../coffee-showcase-vegrafik.png | Bin
.../coffee-showcase-visualpharm.png | Bin
.../index.md} | 54 +-
.../aperture-impression.png | Bin
.../aperture_borderfx.png | Bin
.../aperture_bt.png | Bin 0 -> 80701 bytes
.../index.md} | 20 +-
.../watermark_5.png | Bin 0 -> 658866 bytes
.../watermark_8.png | Bin 0 -> 13417 bytes
.../watermark_aperture.jpg | Bin 0 -> 155262 bytes
.../custom-gravatar.jpg | Bin
.../index.md} | 7 +-
.../index.md} | 5 +-
.../futurama-mac-01.png | Bin
.../futurama-mac-02.png | Bin
.../futurama-mac-03.png | Bin
.../futurama-mac-04.png | Bin
.../futurama-mac-05.png | Bin
.../futurama-mac-06.png | Bin
.../futurama-mac-07.png | Bin
.../futurama-mac-08.png | Bin
.../futurama-mac-09.png | Bin
.../futurama-mac-10.png | Bin
.../futurama-mac-11.png | Bin
.../futurama-mac-12.png | Bin
.../futurama-mac-13.png | Bin
.../futurama-mac-14.png | Bin
.../futurama-mac-15.png | Bin
.../futurama-mac-16.png | Bin
.../futurama-mac-17.png | Bin
.../futurama-mac-18.png | Bin
.../futurama-mac-19.png | Bin
.../futurama-mac-20.png | Bin
.../futurama-mac-21.png | Bin
.../futurama-mac-teaser.png | Bin
.../futurama_gedeon.png | Bin
.../futurama_hawkins1.png | Bin
.../futurama_hawkins2.png | Bin
.../futurama_kremalicious.png | Bin
.../futurama_rawpixels.png | Bin
.../index.md} | 105 +-
.../out_of_whale_oil_detail.png | Bin
.../index.md} | 16 +-
.../vcardsite-arefjdey.png | Bin
.../vcardsite-laurent.png | Bin
.../vcardsite-maximilian.png | Bin
.../vcardsite-mk.png | Bin
.../vcardsite-rogie.png | Bin
.../vcardsite-tim.png | Bin
.../Teaser-Out-Of-Whale-Oil.jpg | Bin
.../index.md} | 10 +-
.../out-of-whale-oil-overview.png | Bin
.../out-of-whale-oil-wall-by-kremalicious.zip | Bin
.../out_of_whale_oil_detail.png | Bin 0 -> 45722 bytes
.../coda-clips-icon-files.zip | Bin
.../codaclips-hud.png | Bin
.../codaclips-icon128.png | Bin
.../codaclips-placeholder.png | Bin
.../codaclips-teaser.png | Bin
.../coffee-cup-icon-kremalicious.png | Bin
.../index.md} | 42 +-
.../share-link-bonanza-coda-clips.zip | Bin
.../share-link-bonanza-html.zip | Bin
.../tutorial-icon.png | Bin
.../Teaser-Twitter-Crisp.jpg | Bin
.../index.md} | 10 +-
.../twitter-crisp-by-kremalicious.zip | Bin
.../Adiumeetie-Dock-Preview.png | Bin
.../Adiumeetie-Teaser-AdiumIcon.png | Bin
.../Adiumeetie-Teaser.jpg | Bin
.../Teaser-Adiumeetie.jpg | Bin
.../adiumeetie-by-kremalicious.zip | Bin
.../index.md} | 12 +-
.../Teaser-Delibar-Icons.jpg | Bin
.../delibar-by-kremalicious.zip | Bin
.../index.md} | 8 +-
.../index.md} | 39 +-
.../wordpress-thumbnail-1.png | Bin
.../wordpress-thumbnail-2.png | Bin
.../wordpress-thumbnail-3.png | Bin
.../wordpress-thumbnail-4.png | Bin
.../wordpress-thumbnail-5.png | Bin
.../Teaser-iPixelPad.png | Bin
.../iPixelPad-Teaser-kremalicious2.jpg | Bin
.../iPixelPad-Teaser1.png | Bin
.../index.md} | 12 +-
.../ipixelpad-homescreen-zoom.png | Bin
.../ipixelpad_by_kremalicious.zip | Bin
.../MomCorp-Walls-Overview.png | Bin
.../Teaser-MomCorp-Wall.png | Bin
.../index.md} | 8 +-
.../momcorp_wall_by_kremalicious.zip | Bin
.../Badged-Teaser-kremalicious.png} | Bin
.../2011-12-15-badged}/badged-settings.png | Bin
.../index.md} | 4 +-
.../index.md} | 22 +-
.../2012-02-26-mk-v2}/mkv2-android-chrome.png | Bin
.../mkv2-android-firefox.png | Bin
.../2012-02-26-mk-v2}/mkv2-android.png | Bin
.../2012-02-26-mk-v2}/mkv2-balls.jpg | Bin
.../2012-02-26-mk-v2}/mkv2-detail.png | Bin
.../mkv2-ipad-touchindicator.jpg | Bin
.../2012-02-26-mk-v2}/mkv2-ipad.png | Bin
.../2012-02-26-mk-v2}/mkv2-iphone.png | Bin
.../mkv2-portfolio-overlay.jpg | Bin
.../mkv2-responsivelayouts.jpg | Bin
.../2012-02-26-mk-v2}/mkv2-teaser-450x250.jpg | Bin
.../2012-02-26-mk-v2}/mkv2-teaser-500x277.jpg | Bin
.../2012-02-26-mk-v2}/mkv2-teaser-540x288.jpg | Bin
.../2012-02-26-mk-v2}/mkv2.jpg | Bin
.../Instagram-Swipe.png | Bin
.../android-galaxy-note.png | Bin
.../android-navigation-buttons.png | Bin
.../index.md} | 8 +-
.../tabs_overview.png | Bin
.../index.md} | 16 +-
.../kremalicious2-photogrid.jpg | Bin
.../kremalicious2-photoposts.jpg | Bin
.../kremalicious2-teaser.jpg | Bin
.../kremalicious2-topicicons.jpg | Bin
.../kremalicious2-typography.jpg | Bin
.../kremaliciouscom-iPad-3.jpg | Bin
.../WordPress-Admin-Icons-Template-Filled.png | Bin
.../index.md} | 4 +-
.../kremalicious-Teaser-WP-Icon-Template.png | Bin
.../index.md} | 4 +-
.../kremalicious-Teaser-WP-Icon-Template.png | Bin 0 -> 16436 bytes
.../wp34_retina_icons.png | Bin
...ows-8-Metro-tile-kremalicious-all-apps.png | Bin
...ws-8-Metro-tile-kremalicious-in-action.png | Bin
.../Windows-8-Metro-tile-kremalicious.png | Bin
.../index.md} | 8 +-
.../kremalicious-Teaser-Metro-Tile.jpg | Bin
.../Google Android License.txt | 18 -
.../Roboto-Regular-webfont.eot | Bin 26220 -> 0 bytes
.../Roboto-Regular-webfont.svg | 147 -
.../Roboto-Regular-webfont.ttf | Bin 26024 -> 0 bytes
.../Roboto-Regular-webfont.woff | Bin 15280 -> 0 bytes
.../_post-kbd.css | 93 +
.../index.md | 4 +-
.../post-kbd.css | 85 -
.../Project-Purple-Dribbble.png | Bin
.../Teaser-Project-Purple.png | Bin
.../index.md} | 22 +-
.../project-purple-ipad-kremalicious.png | Bin
.../project-purple-iphone4-kremalicious.png | Bin
.../project-purple-kremalicious.png | Bin
.../project-purple-kremalicious.zip | Bin
.../project-purple-nexus-kremalicious.png | Bin
.../index.md} | 2 +-
.../kremalicious-Teaser-ezeep.png | Bin
.../buddha-colorscheme.png | Bin
.../buddha-printer.png | Bin
.../index.md} | 4 +-
.../index.md} | 2 +-
.../post-time.png | Bin
.../index.md | 10 +-
.../index.md} | 19 +-
.../teaser-appstorebadges.png | Bin
.../2017-05-16-hyper-mac-pro/index.md | 4 +-
.../2018-11-01-gatsby-plugin-matomo/index.md | 6 +-
.../index.md | 10 +-
.../index.md | 30 +-
.../index.md | 10 +-
.../index.md | 36 +-
content/articles/2020-05-08-uses/index.md | 47 +-
.../2020-05-22-gatsby-redirect-from/index.md | 10 +-
.../index.md | 18 +-
.../favicon-generation-with-astro-teaser.png | Bin 0 -> 28371 bytes
.../index.md | 223 +
content/config.ts | 19 +
content/media/Badged-Teaser-kremalicious.png | Bin 120471 -> 0 bytes
content/media/Delibar-Icons-Teaser.jpg | Bin 72475 -> 0 bytes
content/media/Delibar-Icons-Teaser2.png | Bin 117393 -> 0 bytes
content/media/Twitter-Crisp-16.png | Bin 2823 -> 0 bytes
content/media/adiumeetie-goodies-teaser.png | Bin 102557 -> 0 bytes
content/media/aperture72.png | Bin 10914 -> 0 bytes
content/media/box_download.png | Bin 4659 -> 0 bytes
content/media/camera_obscura_wall_teaser.png | Bin 211867 -> 0 bytes
content/media/cameraobscura11_all_thumb.png | Bin 66289 -> 0 bytes
content/media/cameraobscura_aperture_128.png | Bin 23708 -> 0 bytes
content/media/cameraobscura_teaser1.png | Bin 190628 -> 0 bytes
.../ce2d29f40df411e2ad5812313817873b_7.jpg | Bin 125136 -> 0 bytes
content/media/chives_wallpaper_teaser.png | Bin 198699 -> 0 bytes
content/media/codaclips-icon48.png | Bin 4780 -> 0 bytes
content/media/coffee-cup-icon-teaser.png | Bin 126610 -> 0 bytes
content/media/delibar-website.png | Bin 191403 -> 0 bytes
content/media/deliciouslinks.png | Bin 54288 -> 0 bytes
content/media/diskimage98.png | Bin 8195 -> 0 bytes
content/media/efs_18-200_thumb.png | Bin 38533 -> 0 bytes
.../media/elegancesociale_smashingteaser.png | Bin 114110 -> 0 bytes
content/media/encryption_certificate.png | Bin 5424 -> 0 bytes
content/media/encryption_mail2.png | Bin 2163 -> 0 bytes
content/media/eos_50D_back_thumb.png | Bin 54369 -> 0 bytes
content/media/eos_50D_front_thumb.png | Bin 66488 -> 0 bytes
content/media/filevault98.png | Bin 10373 -> 0 bytes
content/media/firefox-icon.png | Bin 12899 -> 0 bytes
content/media/firefox3promo.png | Bin 125692 -> 0 bytes
content/media/free-pdf.png | Bin 18625 -> 0 bytes
content/media/fry-2009-kremalicious.png | Bin 95945 -> 0 bytes
content/media/hastuzeit-kremalicious2.jpg | Bin 12749 -> 0 bytes
content/media/html-document-icon48.png | Bin 1261 -> 0 bytes
content/media/icon-Facebook.png | Bin 2032 -> 0 bytes
content/media/icon-Twitter2.png | Bin 2036 -> 0 bytes
content/media/icon-deviantART.png | Bin 2042 -> 0 bytes
content/media/icybox_teaser1.png | Bin 140684 -> 0 bytes
content/media/icybox_teaser2_small.png | Bin 104793 -> 0 bytes
content/media/jade_ui_thumb.jpg | Bin 49486 -> 0 bytes
content/media/kremalicious-iconiphone-80.png | Bin 13905 -> 0 bytes
content/media/kremalicious_nav.txt | 8 -
content/media/kremaliciousiphone_thumb.png | Bin 34787 -> 0 bytes
content/media/lens.png | Bin 19591 -> 0 bytes
content/media/mars-u-teaser.png | Bin 177409 -> 0 bytes
content/media/mediaMomCorpTeaser.png | Bin 32671 -> 0 bytes
content/media/niepce_portrait.png | Bin 70748 -> 0 bytes
content/media/nik_silverefex_thumb.png | Bin 64801 -> 0 bytes
content/media/opera-icon.png | Bin 9275 -> 0 bytes
content/media/paypal-logo.jpg | Bin 20190 -> 0 bytes
content/media/ptlens_ui_thumb.jpg | Bin 52234 -> 0 bytes
.../media/republica-banner-125x160_black.png | Bin 3122 -> 0 bytes
.../media/republica10_kremaliciousbanner.png | Bin 6511 -> 0 bytes
content/media/safari4_zoom_thumb.png | Bin 46725 -> 0 bytes
content/media/snippet.png | Bin 15489 -> 0 bytes
content/media/softwareupdate.png | Bin 15500 -> 0 bytes
content/media/softwareupdate_photo200.png | Bin 46240 -> 0 bytes
content/media/teaser-out-of-whale-oil.png | Bin 130290 -> 0 bytes
content/media/teaser_elegance-sociale.png | Bin 109791 -> 0 bytes
content/media/tweetie_select_bubbles.zip | Bin 49354 -> 0 bytes
content/media/twitter-crisp-teaser.jpg | Bin 21739 -> 0 bytes
content/media/twitter-crisp-teaser2.png | Bin 55593 -> 0 bytes
content/media/webinspector_1.png | Bin 191717 -> 0 bytes
content/media/xserve_screwed.png | Bin 81218 -> 0 bytes
.../img_1820-Version-4.jpg | Bin
.../index.md} | 2 +-
.../index.md} | 2 +-
.../schnecke_blatt.jpg | Bin
.../MG_5885_2006-7-23.jpg | Bin
.../index.md} | 2 +-
.../floating-sky-1.jpg | Bin
.../index.md} | 2 +-
.../berliner_bruecke1-HDR-16bit.jpg | Bin
.../index.md} | 2 +-
.../index.md} | 2 +-
...muehle_suhlendorf_HDR_Tonemapped_16bit.jpg | Bin
...ramt_01_HDR_tonemapped_16bit-Version-2.jpg | Bin
.../index.md} | 2 +-
.../MG_9313_2007-02-10.jpg | Bin
.../index.md} | 2 +-
.../MG_1735-Version-2.jpg | Bin
.../index.md} | 2 +-
.../index.md} | 2 +-
.../2008-06-30-stone-head}/stonehead.jpg | Bin
.../2008-07-23-leaf-life}/MG_1920.jpg | Bin
.../index.md} | 2 +-
.../A-Long-Time-Ago.jpg | Bin
.../index.md} | 2 +-
.../2010-03-27-office-desk}/Office-Desk.jpg | Bin
.../index.md} | 2 +-
.../Typeface-condoms.jpg | Bin
.../index.md} | 2 +-
.../2010-08-07-bonsai}/Bonsai-5-Version-2.jpg | Bin
.../index.md} | 2 +-
.../GDR-Helvetica.jpg | Bin
.../index.md} | 2 +-
.../iPhone-Coasters-1-Version-2.jpg | Bin
.../index.md} | 2 +-
.../Basically-The-Monolith-Is-On-My-Desk.jpg | Bin
.../index.md} | 2 +-
.../Free-Monkey-Breath-Not-Soylent-Green.jpg | Bin
.../index.md} | 2 +-
.../Enjoying-Paper.jpg | Bin
.../index.md} | 2 +-
.../Glowing-Star-Inside.jpg | Bin
.../index.md} | 2 +-
.../Historic-Flood-Levels.jpg | Bin
.../index.md} | 2 +-
.../Broken-Nexus-S-Screen.jpg | Bin
.../index.md} | 2 +-
.../7f9397a265d811e1b9f1123138140926_7.jpg | Bin
.../index.md} | 2 +-
.../Blaue-Tuerme-1.jpg | Bin
.../index.md} | 2 +-
.../6313cc1e7db611e180c9123138016265_7.jpg | Bin
.../index.md} | 2 +-
.../de2ac24c7db911e1b9f1123138140926_7.jpg | Bin
.../index.md} | 2 +-
.../aff38e2c7f5311e1b10e123138105d6b_7.jpg | Bin
.../index.md} | 2 +-
content/photos/2012-04-07-buna.md | 5 -
.../44af28f2805b11e18cf91231380fd29b_7.jpg | Bin
content/photos/2012-04-07-buna/index.md | 5 +
.../7838011c80ce11e19e4a12313813ffc0_7.jpg | Bin
.../index.md} | 2 +-
.../97a44d6080b711e181bd12313817987b_7.jpg | Bin
.../index.md} | 2 +-
.../5df6e0a280c911e1a87612313804ec91_7.jpg | Bin
.../index.md} | 2 +-
.../2ba6eeba81b111e1989612313815112c_7.jpg | Bin
.../index.md} | 2 +-
.../7e2b28f881b711e1af7612313813f8e8_7.jpg | Bin
.../index.md} | 2 +-
.../5fc688aa953811e180c9123138016265_7.jpg | Bin
.../index.md} | 2 +-
.../41b5a454a43811e1989612313815112c_7.jpg} | Bin
.../index.md} | 2 +-
.../690fe368a81911e1b2fe1231380205bf_7.jpg | Bin
.../index.md} | 2 +-
.../80a136dabff711e188131231381b5c25_7.jpg | Bin
.../index.md} | 2 +-
.../66a6e0c0d25a11e1a94522000a1e8aaf_7.jpg | Bin
.../index.md} | 2 +-
.../2ca7a094e10f11e1868c12313817a130_7.jpg | Bin
.../index.md} | 2 +-
.../c0c45b6eeea211e1ad8e22000a1cdbb8_7.jpg | Bin
.../index.md} | 2 +-
.../619b3900f92911e1a31922000a1cddf1_7.jpg | Bin
.../index.md} | 2 +-
.../84f9d2c4fb7411e19ca422000a1d0119_7.jpg | Bin
.../index.md} | 2 +-
.../01f8b0b8fcc611e19b5b123138140bce_7.jpg | Bin
.../index.md} | 2 +-
.../6c4003f2fe9911e1ae9122000a1e9e21_7.jpg | Bin
.../index.md} | 2 +-
.../8372983659_da0e88ca79_o.jpg | Bin
.../index.md} | 2 +-
.../8455835942_a9b9100373_o.jpg | Bin
.../index.md} | 2 +-
.../8450618380_83c64006c6_o.jpg | Bin
.../index.md} | 2 +-
.../8776417095_43553c88c2_o.jpg | Bin
.../index.md} | 2 +-
.../8782995066_e90ff6b3ae_o.jpg | Bin
.../index.md} | 2 +-
.../index.md} | 7 +-
.../just-a-normal-sunday.jpg | Bin
.../index.md} | 4 +-
.../2014-03-15-potsdam}/potsdam.jpg | Bin
.../index.md} | 2 +-
.../typographic-diamond.jpg | Bin
.../ai-wei-wei-stools.jpg | Bin
.../index.md} | 2 +-
.../airfield-reference-point.jpg | Bin
.../index.md} | 2 +-
...anton-henning-heimat-schaffen-simpsons.jpg | Bin
.../index.md} | 2 +-
...-thanks-for-the-tip-little-orange-blob.jpg | Bin
.../index.md} | 2 +-
.../index.md} | 2 +-
.../most-surprising-dog-i-know.jpg | Bin
.../index.md} | 2 +-
...obligatory-it-s-summer-in-berlin-photo.jpg | Bin
...hindu-temple-appears-around-the-corner.jpg | Bin
.../index.md} | 2 +-
.../Monstrum_Gameboy_Catherine_Kaleel.jpg | Bin
.../index.md} | 2 +-
.../index.md} | 2 +-
.../tiny_tiny_demons.jpg | Bin
.../index.md} | 4 +-
.../sagrada-familia-ceiling.jpg | Bin
.../index.md} | 2 +-
.../streets-of-el-raval.jpg | Bin
.../a-storm-is-coming.jpg | Bin
.../index.md} | 4 +-
.../index.md} | 2 +-
.../keith-haring-vandalizing-a-wall.jpg | Bin
.../index.md} | 2 +-
.../streets-of-el-born.jpg | Bin
.../index.md} | 2 +-
.../new-passion-facade.jpg | Bin
.../coolhaven-rotterdam.jpg | Bin
.../index.md} | 4 +-
.../behind-the-art.jpg | Bin
.../index.md} | 4 +-
.../2017-02-19-rotterdam-coats.jpg | Bin
.../index.md} | 2 +-
...017-02-21-david-chipperfield-staircase.jpg | Bin
.../index.md} | 2 +-
.../2017-02-26-eu-gotham-city.jpg | Bin
.../index.md} | 2 +-
.../2017-02-27-amsterdam-cliche.jpg | Bin
.../index.md} | 2 +-
.../2017-02-27-its-dark-and-i-dont-exist.jpg | Bin
.../index.md} | 2 +-
.../2017-02-28-stedelijk-museum.jpg | Bin
.../index.md} | 2 +-
...2017-02-28-temple-guardian-rijksmuseum.jpg | Bin
.../index.md} | 2 +-
.../2017-02-28-watching-the-night-watch.jpg | Bin
.../index.md} | 2 +-
.../2017-04-16-hamburgs-elbphilharmonie.jpg | Bin
.../index.md} | 2 +-
.../2017-04-29-palace-scaffolding.jpg | Bin
.../index.md} | 2 +-
...17-07-05-kapaleeshwarar-temple-chennai.jpg | Bin
.../index.md} | 2 +-
...-07-08-kochis-streetart-game-is-strong.jpg | Bin
.../index.md} | 2 +-
...17-07-09-orphaned-elephant-with-friend.jpg | Bin
.../index.md} | 2 +-
.../2017-07-10-kochis-dhobhi-ghat.jpg | Bin
.../index.md} | 2 +-
...7-07-13-mumbai-hand-painted-typography.jpg | Bin
.../index.md} | 2 +-
...7-07-13-obligatory-gate-of-india-photo.jpg | Bin
.../index.md} | 2 +-
...ati-shivaji-maharaj-vastu-sangrahalaya.jpg | Bin
.../index.md} | 2 +-
.../2017-11-10-acropolis-the-erechtheum.jpg | Bin
.../index.md} | 2 +-
.../2017-11-10-acropolis-the-parthenon.jpg | Bin
.../index.md} | 2 +-
.../2017-11-10-acropolis-the-propylaea.jpg | Bin
.../index.md} | 2 +-
...15-el-born-centre-de-cultura-i-memoria.jpg | Bin
.../index.md} | 2 +-
.../2017-12-16-sagrada-familia.jpg | Bin
.../index.md} | 2 +-
.../2017-12-27-sao-paulo-traffic.jpg | Bin
.../index.md} | 2 +-
...01-04-passagem-literaria-da-consolacao.jpg | Bin
.../index.md} | 2 +-
.../2018-01-05-samba-school.jpg | Bin
.../index.md} | 2 +-
.../2018-01-10-smoking-death.jpg | Bin
.../index.md} | 2 +-
.../2018-01-13-teatro-jaragua.jpg | Bin
.../index.md} | 2 +-
...-15-a-revolucao-nao-sera-televisionada.jpg | Bin
.../index.md} | 2 +-
.../2018-01-15-fusca-e-pichacao.jpg | Bin
.../index.md} | 2 +-
.../2018-01-17-boa-viagem.jpg | Bin
.../index.md} | 2 +-
.../2018-01-17-instituto-ricardo-brennand.jpg | Bin
.../index.md} | 2 +-
.../2018-01-24-avenida-paulista-i.jpg | Bin
.../index.md} | 2 +-
.../2018-01-25-avenida-paulista-ii.jpg | Bin
.../index.md} | 2 +-
.../2018-01-26-auditorio-ibirapuera.jpg | Bin
.../index.md} | 2 +-
.../2018-01-26-oca-do-ibirapuera-i.jpg | Bin
.../index.md} | 2 +-
.../2018-01-26-oca-do-ibirapuera-ii.jpg | Bin
.../index.md} | 2 +-
...1-26-pavilhao-das-culturas-brasileiras.jpg | Bin
.../index.md} | 2 +-
...24-muzeul-national-de-arta-al-romaniei.jpg | Bin
.../index.md} | 2 +-
...-muzeul-national-de-istorie-a-romaniei.jpg | Bin
.../index.md} | 2 +-
.../2018-05-01-may-day-kreuzberg.jpg | Bin
.../index.md} | 2 +-
.../2018-06-01-roger-waters-us-them.jpg | Bin
.../index.md} | 2 +-
.../2018-07-02-nine-inch-nails-zitadelle.jpg | Bin
.../index.md} | 2 +-
.../2018-09-01-stasi-museum.jpg | Bin
.../index.md} | 2 +-
.../2018-12-26-elevador-de-santa-justa.jpg | Bin
.../index.md} | 2 +-
.../2018-12-27-palacio-nacional-da-pena.jpg | Bin
.../index.md} | 2 +-
.../2018-12-27-rua-da-prata.jpg | Bin
.../index.md} | 2 +-
.../2018-12-29-gare-do-oriente.jpg | Bin
.../index.md} | 2 +-
.../2019-01-27-all-work-and-no-play.jpg | Bin
.../index.md} | 2 +-
.../2019-02-25-edificio-italia.jpg | Bin
.../index.md} | 2 +-
...zilian-museum-of-sculpture-and-ecology.jpg | Bin
.../index.md} | 2 +-
.../2019-03-13-paraty-mirim.jpg | Bin
.../index.md} | 2 +-
...2019-03-18-catedral-da-se-de-sao-paulo.jpg | Bin
.../index.md} | 2 +-
.../2019-08-18-german-chancellery-ii.jpg | Bin
.../index.md} | 2 +-
.../2019-09-28-vatican-museums.jpg | Bin
.../index.md} | 2 +-
.../2019-09-29-arco-di-costantino.jpg | Bin
.../index.md} | 2 +-
.../2019-09-29-foro-di-cesare.jpg | Bin
.../index.md} | 2 +-
.../2019-11-02-orszaghaz-i.jpg | Bin
.../index.md} | 2 +-
.../2019-11-03-orszaghaz-ii.jpg | Bin
.../index.md} | 2 +-
.../2019-11-03-orszaghaz-iii.jpg | Bin
.../index.md} | 2 +-
.../2019-11-16-helmut-newton-foundation.jpg | Bin
.../index.md} | 2 +-
.../2020-01-05-raw-gelande.jpg | Bin
.../index.md} | 2 +-
.../2020-01-17-balloon-dog.jpg | Bin
.../index.md} | 2 +-
.../2020-01-17-bremen-cathedral.jpg | Bin
.../index.md} | 2 +-
.../2020-06-14-x-marks-the-spot.jpg | Bin
.../index.md} | 2 +-
.../2020-06-28-lines-of-nature.jpg | Bin
.../index.md} | 2 +-
.../{ => 2020-08-13-2020}/2020-08-13-2020.jpg | Bin
.../index.md} | 2 +-
.../2020-08-16-castle-gardens.jpg | Bin
.../index.md} | 2 +-
.../2020-09-12-friedrichshain.jpg | Bin
.../index.md} | 2 +-
.../2020-10-11-charite.jpg | Bin
.../index.md} | 2 +-
.../2020-10-25-letters-change.jpg | Bin
.../index.md} | 2 +-
.../2020-11-29-wall-memorial.jpg | Bin
.../index.md} | 2 +-
.../2020-12-22-tier-pandemic.jpg | Bin
.../index.md} | 2 +-
.../2021-02-21-reflections.jpg | Bin
.../index.md} | 2 +-
.../2021-07-07-museum-neuruppin-staircase.jpg | Bin
.../index.md} | 2 +-
.../2021-08-05-maidan-nezalezhnosti.jpg | Bin
.../index.md} | 2 +-
...mihayila-ta-ukrayinskih-novomuchenikiv.jpg | Bin
.../index.md} | 2 +-
.../2021-08-08-kyiv-funicular.jpg | Bin
.../index.md} | 2 +-
.../2021-08-08-kyiv-underground.jpg | Bin
.../index.md} | 2 +-
.../2021-08-09-kiyiv-pasazhirskij.jpg | Bin
.../index.md} | 2 +-
.../2021-08-17-horticya.jpg | Bin
.../index.md} | 2 +-
.../2021-08-17-zaporizhzhya.jpg | Bin
.../index.md} | 2 +-
.../2021-09-26-arc-de-triomphe-wrapped.jpg | Bin
.../index.md} | 2 +-
.../2021-09-26-centre-pompidou-pigeons.jpg | Bin
.../index.md} | 2 +-
.../2021-09-26-louvre-i.jpg | Bin
.../index.md} | 2 +-
.../2021-09-26-tour-eiffel.jpg | Bin
.../index.md} | 2 +-
.../2021-09-29-louvre-ii.jpg | Bin
.../index.md} | 2 +-
.../2021-09-30-tgv-euroduplex.jpg | Bin
.../index.md} | 2 +-
...-04-santa-maria-la-real-de-la-almudena.jpg | Bin
.../index.md} | 2 +-
.../2021-10-09-ponte-da-arrabida.jpg | Bin
.../index.md} | 2 +-
.../2021-10-11-garagem.jpg | Bin
.../index.md} | 2 +-
.../2021-10-12-ponte-de-dom-luis-i.jpg | Bin
.../index.md} | 2 +-
.../2021-10-14-lince-iberico.jpg | Bin
.../index.md} | 2 +-
.../2021-10-16-gare-do-oriente-ii.jpg | Bin
.../index.md} | 2 +-
.../{ => 2021-10-17-maat}/2021-10-17-maat.jpg | Bin
.../index.md} | 2 +-
.../2021-10-17-padrao-dos-descobrimentos.jpg | Bin
.../index.md} | 2 +-
.../2021-10-17-ponte-25-de-abril.jpg | Bin
.../index.md} | 2 +-
.../2021-10-17-shiny-colonialism.jpg | Bin
.../index.md} | 2 +-
.../2021-10-23-santuario-de-cristo-rei.jpg | Bin
.../index.md} | 2 +-
.../2021-10-29-ponte-vasco-da-gama.jpg | Bin
.../index.md} | 2 +-
.../2021-10-31-sagrada-familia-ii.jpg | Bin
.../index.md} | 2 +-
.../2021-11-06-mae-d-agua.jpg | Bin
.../index.md} | 2 +-
.../2021-11-21-assembleia-da-republica.jpg | Bin
.../index.md} | 2 +-
.../2021-11-25-praca-do-comercio.jpg | Bin
.../index.md} | 2 +-
.../2021-11-26-forever-bicycles.jpg | Bin
.../index.md} | 2 +-
.../2021-11-26-law-of-the-journey.jpg | Bin
.../index.md} | 2 +-
gatsby-browser.tsx | 7 -
gatsby-config.ts | 220 -
gatsby-node.ts | 180 -
gatsby-ssr.tsx | 26 -
gatsby/algolia.ts | 89 -
gatsby/createExif.ts | 148 -
gatsby/createMarkdownFields.ts | 72 -
gatsby/createPages.ts | 133 -
gatsby/feeds.ts | 75 -
gatsby/sources.ts | 63 -
package-lock.json | 51935 ++--------------
package.json | 148 +-
{static => public}/robots.txt | 2 +-
{static => public}/sw.js | 0
scripts/create-icons/Props.d.ts | 320 +
scripts/create-icons/index.test.ts | 39 +
scripts/create-icons/index.ts | 103 +
scripts/create-icons/svg.ts | 87 +
scripts/create-symlinks.sh | 26 +
scripts/deploy-s3.sh | 31 +-
scripts/move-downloads.test.ts | 43 +
scripts/move-downloads.ts | 73 +
scripts/new.ts | 119 -
scripts/new/createArticlePost.ts | 52 +
scripts/new/createPhotoPost.ts | 77 +
scripts/new/index.test.ts | 78 +
scripts/new/index.ts | 26 +
scripts/{ => new}/new-article.md | 2 +-
scripts/{ => new}/new-photo.md | 5 +-
scripts/redirect-from.test.ts | 32 +
scripts/redirect-from.ts | 76 +
src/@types/Image.d.ts | 7 -
src/@types/Post.d.ts | 8 -
src/@types/css.d.ts | 1 -
.../{node_modules.d.ts => dmsdec/index.d.ts} | 5 -
src/@types/global.d.ts | 9 -
src/@types/node-iptc/index.d.ts | 71 +
src/components/BackButton.astro | 24 +
src/components/Changelog/index.astro | 50 +
.../index.module.css} | 17 +-
src/components/Copy.astro | 63 +
src/components/{atoms => }/Divider.module.css | 2 +-
src/components/Donation/Coin.astro | 42 +
src/components/Donation/Web3.tsx | 15 +
.../Web3Donation/Alert.module.css | 0
.../Web3Donation/Alert.test.tsx | 2 +-
.../Web3Donation/Alert.tsx | 8 +-
.../Web3Donation/Conversion.module.css | 4 +-
.../Web3Donation/Conversion.test.tsx | 4 +-
.../Web3Donation/Conversion.tsx | 30 +-
.../Web3Donation/InputGroup.module.css | 4 +-
.../Donation/Web3Donation/InputGroup.test.tsx | 39 +
.../Web3Donation/InputGroup.tsx | 22 +-
.../Web3Donation/index.module.css | 14 +-
.../Donation/Web3Donation/index.test.tsx | 27 +
.../Web3Donation/index.tsx | 31 +-
src/components/Exif/ExifData.astro | 15 +
src/components/Exif/ExifMap.test.tsx | 20 +
src/components/Exif/ExifMap.tsx | 57 +
src/components/Exif/index.astro | 47 +
.../Exif.module.css => Exif/index.module.css} | 5 +-
src/components/Footer/Networks.astro | 30 +
.../{molecules => Footer}/Networks.module.css | 0
src/components/Footer/Vcard.astro | 28 +
.../{molecules => Footer}/Vcard.module.css | 6 +-
src/components/Footer/index.astro | 30 +
.../index.module.css} | 0
src/components/Hamburger.astro | 84 +
src/components/Header/index.astro | 21 +
.../index.module.css} | 2 +-
.../index.module.css} | 2 +-
src/components/Input/index.test.tsx | 8 +
.../{atoms/Input.tsx => Input/index.tsx} | 4 +-
src/components/Layout.test.tsx | 7 -
src/components/Layout.tsx | 20 -
src/components/Menu/index.astro | 39 +
.../Menu.module.css => Menu/index.module.css} | 5 +-
src/components/More.astro | 42 +
src/components/Pagination/PageNumber.astro | 18 +
src/components/Pagination/PrevNext.astro | 19 +
src/components/Pagination/index.astro | 25 +
.../index.module.css} | 1 -
src/components/PhotoTeaser.astro | 43 +
src/components/Picture/index.astro | 105 +
.../index.module.css} | 18 +-
src/components/PostTeaser/index.astro | 38 +
.../index.module.css} | 12 +-
src/components/RelatedPosts/index.astro | 65 +
.../index.module.css} | 14 +-
.../Results/Empty.module.css} | 0
.../Results/Empty.tsx} | 16 +-
.../Results/index.module.css} | 26 +-
src/components/Search/Results/index.tsx | 49 +
.../Search.module.css} | 12 +
src/components/Search/Search.test.tsx | 64 +
src/components/Search/Search.tsx | 97 +
src/components/Search/index.astro | 50 +
src/components/Tag.astro | 38 +
src/components/ThemeSwitch/index.astro | 23 +
.../index.module.css} | 4 +
src/components/ThemeSwitch/theme.cjs | 74 +
src/components/ThemeSwitch/theme.test.ts | 110 +
src/components/Time.astro | 20 +
src/components/Toc.astro | 29 +
src/components/atoms/Changelog.test.tsx | 16 -
src/components/atoms/Changelog.tsx | 88 -
src/components/atoms/Copy.module.css | 29 -
src/components/atoms/Copy.test.tsx | 10 -
src/components/atoms/Copy.tsx | 21 -
src/components/atoms/Exif.test.tsx | 24 -
src/components/atoms/Exif.tsx | 58 -
src/components/atoms/ExifMap.tsx | 45 -
src/components/atoms/Hamburger.module.css | 70 -
src/components/atoms/Hamburger.tsx | 23 -
src/components/atoms/HeadMeta/SchemaOrg.tsx | 62 -
src/components/atoms/HeadMeta/index.tsx | 72 -
src/components/atoms/Icon.module.css | 10 -
src/components/atoms/Icon.test.tsx | 21 -
src/components/atoms/Icon.tsx | 69 -
src/components/atoms/Image.tsx | 56 -
src/components/atoms/Input.test.tsx | 8 -
src/components/atoms/Tag.module.css | 20 -
src/components/atoms/Tag.tsx | 22 -
src/components/atoms/Time.tsx | 13 -
src/components/atoms/Transitions.ts | 32 -
src/components/atoms/Typekit.tsx | 17 -
src/components/layouts/Archive.astro | 58 +
src/components/layouts/Base/Head.astro | 119 +
src/components/layouts/Base/SchemaOrg.astro | 51 +
src/components/layouts/Base/index.astro | 35 +
.../Base/index.module.css} | 41 +-
src/components/layouts/Post/Action.astro | 57 +
src/components/layouts/Post/Actions.astro | 46 +
.../layouts/Post/Actions.module.css | 16 +
src/components/layouts/Post/Date.astro | 25 +
src/components/layouts/Post/LinkActions.astro | 20 +
.../Post/LinkActions.module.css | 1 -
src/components/layouts/Post/Meta.astro | 44 +
.../Post/Meta.module.css | 0
src/components/layouts/Post/PrevNext.astro | 41 +
.../Post/PrevNext.module.css | 0
src/components/layouts/Post/Title.astro | 36 +
.../Post/Title.module.css | 5 +-
src/components/layouts/Post/index.astro | 74 +
src/components/layouts/Post/index.module.css | 66 +
src/components/molecules/Menu.tsx | 39 -
src/components/molecules/Networks.test.tsx | 15 -
src/components/molecules/Networks.tsx | 39 -
src/components/molecules/Pagination.tsx | 72 -
src/components/molecules/PostDate.module.css | 6 -
src/components/molecules/PostDate.tsx | 19 -
src/components/molecules/PostTeaser.test.tsx | 20 -
src/components/molecules/PostTeaser.tsx | 65 -
.../molecules/RelatedPosts.test.tsx | 16 -
src/components/molecules/RelatedPosts.tsx | 104 -
.../molecules/Search/SearchButton.module.css | 22 -
.../molecules/Search/SearchButton.tsx | 16 -
.../molecules/Search/SearchInput.tsx | 34 -
.../molecules/Search/SearchResults.tsx | 79 -
.../molecules/Search/index.module.css | 46 -
src/components/molecules/Search/index.tsx | 71 -
src/components/molecules/ThemeSwitch.test.tsx | 20 -
src/components/molecules/ThemeSwitch.tsx | 33 -
src/components/molecules/Vcard.tsx | 53 -
.../Web3Donation/InputGroup.test.tsx | 20 -
src/components/organisms/Footer.test.tsx | 7 -
src/components/organisms/Footer.tsx | 40 -
src/components/organisms/Header.test.tsx | 19 -
src/components/organisms/Header.tsx | 25 -
src/components/templates/Archive.module.css | 14 -
src/components/templates/Archive.test.tsx | 23 -
src/components/templates/Archive.tsx | 71 -
src/components/templates/Page.module.css | 9 -
src/components/templates/Page.tsx | 19 -
src/components/templates/Photos.module.css | 26 -
src/components/templates/Photos.test.tsx | 24 -
src/components/templates/Photos.tsx | 107 -
.../templates/Post/Actions.module.css | 57 -
src/components/templates/Post/Actions.tsx | 53 -
.../templates/Post/Content.module.css | 11 -
src/components/templates/Post/Content.tsx | 38 -
src/components/templates/Post/Lead.module.css | 9 -
src/components/templates/Post/Lead.tsx | 31 -
src/components/templates/Post/LinkActions.tsx | 24 -
src/components/templates/Post/Meta.tsx | 45 -
src/components/templates/Post/More.module.css | 25 -
src/components/templates/Post/More.tsx | 19 -
src/components/templates/Post/PrevNext.tsx | 39 -
src/components/templates/Post/Title.tsx | 40 -
src/components/templates/Post/Toc.module.css | 15 -
src/components/templates/Post/Toc.tsx | 18 -
.../templates/Post/index.module.css | 17 -
src/components/templates/Post/index.test.tsx | 31 -
src/components/templates/Post/index.tsx | 161 -
src/env.d.ts | 3 +
src/helpers/umami.ts | 9 -
src/helpers/wrapPageElement.test.tsx | 13 -
src/helpers/wrapPageElement.tsx | 11 -
src/hooks/useDarkMode.ts | 84 -
src/hooks/useSiteMetadata.ts | 37 -
src/images/apple-touch-icon.png | Bin 10894 -> 0 bytes
src/images/favicon-16x16.png | Bin 819 -> 0 bytes
src/images/favicon-32x32.png | Bin 1349 -> 0 bytes
src/images/favicon-96x96.png | Bin 5039 -> 0 bytes
src/images/favicon-mask.svg | 11 -
src/images/favicon.png | Bin 0 -> 3706 bytes
src/images/favicon.svg | 29 +
src/images/kremalicious1024.png | Bin 263997 -> 0 bytes
src/images/kremalicious512.png | Bin 87864 -> 0 bytes
src/images/krlcus-cloud16.png | Bin 708 -> 0 bytes
src/images/krlcus-cloud32.png | Bin 1608 -> 0 bytes
src/images/logo-kremalicious-g-profile.png | Bin 66164 -> 0 bytes
src/images/metro-tile.png | Bin 1751 -> 0 bytes
src/images/touch-icon-192x192.png | Bin 18372 -> 0 bytes
src/lib/astro/astro.test.ts | 75 +
src/lib/astro/getAllPosts.ts | 14 +
src/lib/astro/getAllPostsForSearch.ts | 36 +
src/lib/astro/getAllTags.ts | 30 +
src/lib/astro/getPostsByTag.ts | 9 +
src/lib/astro/index.ts | 6 +
src/lib/astro/loadAndFormatCollection.ts | 80 +
src/lib/astro/sortPosts.ts | 14 +
src/lib/exif/format.ts | 92 +
src/lib/exif/index.ts | 35 +
src/lib/exif/types.d.ts | 25 +
src/lib/feed.test.ts | 34 +
src/lib/feed.ts | 14 +
src/lib/github.test.ts | 59 +
src/lib/github.ts | 53 +
src/lib/markdown.ts | 7 +
src/{helpers => lib}/rainbowkit.ts | 18 +-
src/lib/remark-lead-paragraph.test.ts | 52 +
src/lib/remark-lead-paragraph.ts | 50 +
src/lib/remark-toc.test.ts | 81 +
src/lib/remark-toc.ts | 45 +
src/lib/slugify.test.ts | 23 +
src/lib/slugify.ts | 8 +
src/lib/umami.test.ts | 36 +
src/lib/umami.ts | 11 +
src/pages/404.astro | 102 +
src/pages/404.module.css | 82 -
src/pages/404.tsx | 28 -
src/pages/[...slug].astro | 33 +
src/pages/__tests__/404.test.tsx | 13 -
src/pages/__tests__/index.test.tsx | 12 -
src/pages/__tests__/tags.test.tsx | 22 -
src/pages/api/posts.ts | 7 +
src/pages/archive/[page].astro | 22 +
src/pages/archive/index.astro | 3 +
src/pages/favicon.ico.ts | 15 +
src/pages/feed.json.ts | 50 +
src/pages/feed.xml.ts | 27 +
src/pages/index.astro | 61 +
src/pages/index.module.css | 46 -
src/pages/index.tsx | 83 -
src/pages/manifest.json.ts | 35 +
src/pages/photos/[page].astro | 22 +
src/pages/photos/index.astro | 3 +
.../{styleguide/index.md => styleguide.md} | 6 +-
src/pages/tags.module.css | 15 -
src/pages/tags.tsx | 47 -
src/pages/tags/[tag].astro | 38 +
src/pages/tags/index.astro | 48 +
src/pages/thanks.astro | 53 +
src/pages/thanks.module.css | 67 -
src/pages/thanks.tsx | 84 -
src/stores/search.ts | 3 +
src/{global => styles}/_alerts.css | 6 +-
src/{global => styles}/_buttons.css | 4 +-
src/{global => styles}/_code.css | 77 +-
src/{global => styles}/_variables.css | 12 +-
src/{global => styles}/global.css | 38 +-
src/{global => styles}/imports.css | 0
static/apple-touch-icon.png | Bin 10894 -> 0 bytes
static/favicon.ico | Bin 6518 -> 0 bytes
test/__fixtures__/image-with-metadata.jpg | Bin 0 -> 281289 bytes
test/__fixtures__/new-article.md | 11 +
test/__fixtures__/new-photo.md | 12 +
.../__mocks__/@rainbow-me/rainbowkit.js | 12 +-
test/__mocks__/wagmi.ts | 105 +
test/e2e/404.spec.ts | 15 +
...ches-screenshot-1-Mobile-Chrome-darwin.png | Bin 0 -> 22369 bytes
...tches-screenshot-1-Mobile-Chrome-linux.png | Bin 0 -> 21596 bytes
...ches-screenshot-1-Mobile-Safari-darwin.png | Bin 0 -> 26003 bytes
...tches-screenshot-1-Mobile-Safari-linux.png | Bin 0 -> 18802 bytes
.../matches-screenshot-1-chromium-darwin.png | Bin 0 -> 29096 bytes
.../matches-screenshot-1-chromium-linux.png | Bin 0 -> 28222 bytes
.../matches-screenshot-1-firefox-darwin.png | Bin 0 -> 42839 bytes
.../matches-screenshot-1-firefox-linux.png | Bin 0 -> 42771 bytes
.../matches-screenshot-1-webkit-darwin.png | Bin 0 -> 42329 bytes
.../matches-screenshot-1-webkit-linux.png | Bin 0 -> 24594 bytes
test/e2e/header.spec.ts | 87 +
...ches-screenshot-1-Mobile-Chrome-darwin.png | Bin 0 -> 2307 bytes
...tches-screenshot-1-Mobile-Chrome-linux.png | Bin 0 -> 3594 bytes
...ches-screenshot-1-Mobile-Safari-darwin.png | Bin 0 -> 4068 bytes
...tches-screenshot-1-Mobile-Safari-linux.png | Bin 0 -> 3239 bytes
.../matches-screenshot-1-chromium-darwin.png | Bin 0 -> 5412 bytes
.../matches-screenshot-1-chromium-linux.png | Bin 0 -> 6726 bytes
.../matches-screenshot-1-firefox-darwin.png | Bin 0 -> 6459 bytes
.../matches-screenshot-1-firefox-linux.png | Bin 0 -> 6449 bytes
.../matches-screenshot-1-webkit-darwin.png | Bin 0 -> 8541 bytes
.../matches-screenshot-1-webkit-linux.png | Bin 0 -> 6316 bytes
test/e2e/index.spec.ts | 35 +
test/e2e/photos.spec.ts | 9 +
test/e2e/post.spec.ts | 30 +
test/e2e/thanks.spec.ts | 15 +
test/playwright.config.ts | 56 +
test/vitest.config.ts | 33 +
test/vitest.setup.ts | 25 +
tsconfig.json | 48 +-
vendor/polar-0.0.6.vsix | Bin 955804 -> 0 bytes
1203 files changed, 12947 insertions(+), 54100 deletions(-)
rename .markdownlint.json => .config/.markdownlint.json (100%)
rename .stylelintrc => .config/.stylelintrc.json (100%)
create mode 100644 .config/astro.config.ts
create mode 100644 .config/aws_redirects.xml
rename config.ts => .config/blog.config.ts (81%)
rename .eslintrc => .eslintrc.json (50%)
delete mode 100644 .jest/__fixtures__/avatar.json
delete mode 100644 .jest/__fixtures__/github.json
delete mode 100644 .jest/__fixtures__/home.json
delete mode 100644 .jest/__fixtures__/link.json
delete mode 100644 .jest/__fixtures__/meta.json
delete mode 100644 .jest/__fixtures__/photos.json
delete mode 100644 .jest/__fixtures__/post.json
delete mode 100644 .jest/__fixtures__/postWithMore.json
delete mode 100644 .jest/__fixtures__/posts.json
delete mode 100644 .jest/__mocks__/file.js
delete mode 100644 .jest/__mocks__/gatsby.js
delete mode 100644 .jest/__mocks__/matchMedia.ts
delete mode 100644 .jest/__mocks__/svgr.js
delete mode 100644 .jest/__mocks__/wagmi.js
delete mode 100644 .jest/babel.config.js
delete mode 100644 .jest/jest.config.js
delete mode 100644 .jest/loadershim.js
delete mode 100644 .jest/setup-test-env.ts
delete mode 100644 .jest/testRender.ts
delete mode 100644 .prettierrc
create mode 100644 .prettierrc.json
delete mode 100644 _aws/redirects.xml
create mode 100644 content/_schemas.ts
rename content/articles/{2007-03-01-adjustment-tool-guide-for-aperture-152.md => 2007-03-01-adjustment-tool-guide-for-aperture-152/index.md} (100%)
rename content/{media => articles/2007-06-11-finally-a-universal-scanner-driver-for-the-canon-canoscan-lide-500f-for-intel-macs}/canon1.png (100%)
rename content/{media => articles/2007-06-11-finally-a-universal-scanner-driver-for-the-canon-canoscan-lide-500f-for-intel-macs}/canon2.png (100%)
rename content/{media => articles/2007-06-11-finally-a-universal-scanner-driver-for-the-canon-canoscan-lide-500f-for-intel-macs}/canon3.png (100%)
rename content/{media => articles/2007-06-11-finally-a-universal-scanner-driver-for-the-canon-canoscan-lide-500f-for-intel-macs}/canon4.png (100%)
rename content/{media => articles/2007-06-11-finally-a-universal-scanner-driver-for-the-canon-canoscan-lide-500f-for-intel-macs}/canoscanlide500f.jpg (100%)
rename content/articles/{2007-06-11-finally-a-universal-scanner-driver-for-the-canon-canoscan-lide-500f-for-intel-macs.md => 2007-06-11-finally-a-universal-scanner-driver-for-the-canon-canoscan-lide-500f-for-intel-macs/index.md} (91%)
rename content/articles/{2008-02-26-how-to-quickly-generate-encrypted-logins-on-a-mac-for-htaccess-protected-sites.md => 2008-02-26-how-to-quickly-generate-encrypted-logins-on-a-mac-for-htaccess-protected-sites/index.md} (100%)
rename content/articles/{2008-03-30-launch-of-kremaliciouscom.md => 2008-03-30-launch-of-kremaliciouscom/index.md} (100%)
rename content/articles/{2008-03-31-love-the-parallax.md => 2008-03-31-love-the-parallax/index.md} (80%)
rename content/{media => articles/2008-03-31-love-the-parallax}/parallax.png (100%)
rename content/{media => articles/2008-04-04-aperture-file-types}/Teaser-Aperture-File-Types.jpg (100%)
rename content/{media => articles/2008-04-04-aperture-file-types}/aperturefiletypes_by_kremalicious.zip (100%)
rename content/articles/{2008-04-04-aperture-file-types.md => 2008-04-04-aperture-file-types/index.md} (80%)
rename content/articles/{2008-04-08-pulitzer-price-winners-2008-announced-various-photographers-awarded.md => 2008-04-08-pulitzer-price-winners-2008-announced-various-photographers-awarded/index.md} (96%)
rename content/{media => articles/2008-04-08-pulitzer-price-winners-2008-announced-various-photographers-awarded}/pulitzer.png (100%)
rename content/{media => articles/2008-04-09-changing-the-image-icons-in-mac-os-x-leopard}/PS_CR2FileIcon.png (100%)
rename content/{media => articles/2008-04-09-changing-the-image-icons-in-mac-os-x-leopard}/aperturefiletypes.png (100%)
rename content/articles/{2008-04-09-changing-the-image-icons-in-mac-os-x-leopard.md => 2008-04-09-changing-the-image-icons-in-mac-os-x-leopard/index.md} (53%)
rename content/{media => articles/2008-04-09-changing-the-image-icons-in-mac-os-x-leopard}/openwithpreview.png (100%)
rename content/{media => articles/2008-04-09-changing-the-image-icons-in-mac-os-x-leopard}/pathphotoshop.png (100%)
rename content/{media => articles/2008-04-09-changing-the-image-icons-in-mac-os-x-leopard}/pathpreview.png (100%)
rename content/{media => articles/2008-04-09-changing-the-image-icons-in-mac-os-x-leopard}/raw.png (100%)
rename content/{media => articles/2008-04-14-new-automation-helper-for-apples-aperture-released}/apassis.png (100%)
rename content/articles/{2008-04-14-new-automation-helper-for-apples-aperture-released.md => 2008-04-14-new-automation-helper-for-apples-aperture-released/index.md} (72%)
rename content/articles/{2008-04-15-webkit-team-introduced-css-based-gradients.md => 2008-04-15-webkit-team-introduced-css-based-gradients/index.md} (88%)
rename content/{media => articles/2008-04-15-webkit-team-introduced-css-based-gradients}/webkit.png (100%)
rename content/{media => articles/2008-04-17-make-cool-and-clever-text-effects-with-css-text-shadow}/css.png (100%)
rename content/articles/{2008-04-17-make-cool-and-clever-text-effects-with-css-text-shadow.md => 2008-04-17-make-cool-and-clever-text-effects-with-css-text-shadow/index.md} (92%)
rename content/{media => articles/2008-04-17-make-cool-and-clever-text-effects-with-css-text-shadow}/navbar-kremalicious.png (100%)
rename content/{media => articles/2008-04-17-make-cool-and-clever-text-effects-with-css-text-shadow}/text-shadow-1.png (100%)
rename content/{media => articles/2008-04-17-make-cool-and-clever-text-effects-with-css-text-shadow}/text-shadow-10.png (100%)
rename content/{media => articles/2008-04-17-make-cool-and-clever-text-effects-with-css-text-shadow}/text-shadow-11.png (100%)
rename content/{media => articles/2008-04-17-make-cool-and-clever-text-effects-with-css-text-shadow}/text-shadow-12.png (100%)
rename content/{media => articles/2008-04-17-make-cool-and-clever-text-effects-with-css-text-shadow}/text-shadow-13.png (100%)
rename content/{media => articles/2008-04-17-make-cool-and-clever-text-effects-with-css-text-shadow}/text-shadow-14.png (100%)
rename content/{media => articles/2008-04-17-make-cool-and-clever-text-effects-with-css-text-shadow}/text-shadow-15.png (100%)
rename content/{media => articles/2008-04-17-make-cool-and-clever-text-effects-with-css-text-shadow}/text-shadow-2.png (100%)
rename content/{media => articles/2008-04-17-make-cool-and-clever-text-effects-with-css-text-shadow}/text-shadow-3.png (100%)
rename content/{media => articles/2008-04-17-make-cool-and-clever-text-effects-with-css-text-shadow}/text-shadow-4.png (100%)
rename content/{media => articles/2008-04-17-make-cool-and-clever-text-effects-with-css-text-shadow}/text-shadow-5.png (100%)
rename content/{media => articles/2008-04-17-make-cool-and-clever-text-effects-with-css-text-shadow}/text-shadow-6.png (100%)
rename content/{media => articles/2008-04-17-make-cool-and-clever-text-effects-with-css-text-shadow}/text-shadow-7.png (100%)
rename content/{media => articles/2008-04-17-make-cool-and-clever-text-effects-with-css-text-shadow}/text-shadow-8.png (100%)
rename content/{media => articles/2008-04-17-make-cool-and-clever-text-effects-with-css-text-shadow}/text-shadow-9.png (100%)
rename content/{media => articles/2008-04-17-make-cool-and-clever-text-effects-with-css-text-shadow}/text-shadow-hack.png (100%)
rename content/{media => articles/2008-04-17-make-cool-and-clever-text-effects-with-css-text-shadow}/textshadow-apple.png (100%)
delete mode 100644 content/articles/2008-04-22-apple-releases-sdk-for-aperture-21.md
rename content/{media => articles/2008-04-22-apple-releases-sdk-for-aperture-21}/aperture97.png (100%)
create mode 100644 content/articles/2008-04-22-apple-releases-sdk-for-aperture-21/index.md
rename content/articles/{2008-04-28-more-awesomeness-from-the-webkit-team-css-masks.md => 2008-04-28-more-awesomeness-from-the-webkit-team-css-masks/index.md} (58%)
create mode 100644 content/articles/2008-04-28-more-awesomeness-from-the-webkit-team-css-masks/webkit.png
rename content/{media => articles/2008-05-04-using-the-canoscan-lide-500f-with-mac-os-x-leopard}/aperturescan.png (100%)
rename content/{media => articles/2008-05-04-using-the-canoscan-lide-500f-with-mac-os-x-leopard}/canondrivercontents.png (100%)
create mode 100644 content/articles/2008-05-04-using-the-canoscan-lide-500f-with-mac-os-x-leopard/canoscanlide500f.jpg
rename content/articles/{2008-05-04-using-the-canoscan-lide-500f-with-mac-os-x-leopard.md => 2008-05-04-using-the-canoscan-lide-500f-with-mac-os-x-leopard/index.md} (93%)
rename content/{media => articles/2008-05-04-using-the-canoscan-lide-500f-with-mac-os-x-leopard}/scangear16bit.png (100%)
rename content/{media => articles/2008-05-04-using-the-canoscan-lide-500f-with-mac-os-x-leopard}/scangear48bit.png (100%)
rename content/{media => articles/2008-05-04-using-the-canoscan-lide-500f-with-mac-os-x-leopard}/scangear_color.png (100%)
rename content/{media => articles/2008-05-04-using-the-canoscan-lide-500f-with-mac-os-x-leopard}/scangearwindow.png (100%)
rename content/{media => articles/2008-05-04-using-the-canoscan-lide-500f-with-mac-os-x-leopard}/scanwindow_preview.png (100%)
delete mode 100644 content/articles/2008-05-05-scan-images-directly-into-apple-aperture.md
rename content/{media => articles/2008-05-05-scan-images-directly-into-apple-aperture}/apertureimport_automatic.png (100%)
rename content/{media => articles/2008-05-05-scan-images-directly-into-apple-aperture}/apertureimport_automator.png (100%)
rename content/{media => articles/2008-05-05-scan-images-directly-into-apple-aperture}/apertureimportplugin.png (100%)
create mode 100644 content/articles/2008-05-05-scan-images-directly-into-apple-aperture/aperturescan.png
rename content/{media => articles/2008-05-05-scan-images-directly-into-apple-aperture}/automator.png (100%)
create mode 100644 content/articles/2008-05-05-scan-images-directly-into-apple-aperture/index.md
rename content/articles/{2008-05-07-high-quality-watermarks-with-aperture.md => 2008-05-07-high-quality-watermarks-with-aperture/index.md} (84%)
rename content/{media => articles/2008-05-07-high-quality-watermarks-with-aperture}/watermark_1.png (100%)
rename content/{media => articles/2008-05-07-high-quality-watermarks-with-aperture}/watermark_2.png (100%)
rename content/{media => articles/2008-05-07-high-quality-watermarks-with-aperture}/watermark_3.png (100%)
rename content/{media => articles/2008-05-07-high-quality-watermarks-with-aperture}/watermark_4.png (100%)
rename content/{media => articles/2008-05-07-high-quality-watermarks-with-aperture}/watermark_5.png (100%)
rename content/{media => articles/2008-05-07-high-quality-watermarks-with-aperture}/watermark_6.png (100%)
rename content/{media => articles/2008-05-07-high-quality-watermarks-with-aperture}/watermark_7.png (100%)
rename content/{media => articles/2008-05-07-high-quality-watermarks-with-aperture}/watermark_8.png (100%)
rename content/{media => articles/2008-05-07-high-quality-watermarks-with-aperture}/watermark_9.png (100%)
rename content/{media => articles/2008-05-07-high-quality-watermarks-with-aperture}/watermark_aperture.jpg (100%)
rename content/{media => articles/2008-05-07-high-quality-watermarks-with-aperture}/watermark_example_by_kremalicious.zip (100%)
rename content/{media => articles/2008-05-07-high-quality-watermarks-with-aperture}/watermarkexample_final.jpg (100%)
rename content/{media => articles/2008-05-07-high-quality-watermarks-with-aperture}/watermarkexample_v2.jpg (100%)
rename content/{media => articles/2008-05-07-high-quality-watermarks-with-aperture}/watermarkexample_v3.jpg (100%)
rename content/{media => articles/2008-05-07-high-quality-watermarks-with-aperture}/watermarkexample_v4.jpg (100%)
rename content/articles/{2008-05-07-nik-ships-viveza-as-the-first-available-aperture-editing-plugin.md => 2008-05-07-nik-ships-viveza-as-the-first-available-aperture-editing-plugin/index.md} (78%)
rename content/{media => articles/2008-05-07-nik-ships-viveza-as-the-first-available-aperture-editing-plugin}/viveza.png (100%)
rename content/{media => articles/2008-05-07-the-15-best-new-features-of-aperture-2}/aperture15_1.png (100%)
rename content/{media => articles/2008-05-07-the-15-best-new-features-of-aperture-2}/aperture15_10.png (100%)
rename content/{media => articles/2008-05-07-the-15-best-new-features-of-aperture-2}/aperture15_11.png (100%)
rename content/{media => articles/2008-05-07-the-15-best-new-features-of-aperture-2}/aperture15_12.png (100%)
rename content/{media => articles/2008-05-07-the-15-best-new-features-of-aperture-2}/aperture15_13.png (100%)
rename content/{media => articles/2008-05-07-the-15-best-new-features-of-aperture-2}/aperture15_14.jpg (100%)
rename content/{media => articles/2008-05-07-the-15-best-new-features-of-aperture-2}/aperture15_15.png (100%)
rename content/{media => articles/2008-05-07-the-15-best-new-features-of-aperture-2}/aperture15_2.png (100%)
rename content/{media => articles/2008-05-07-the-15-best-new-features-of-aperture-2}/aperture15_3.png (100%)
rename content/{media => articles/2008-05-07-the-15-best-new-features-of-aperture-2}/aperture15_4.png (100%)
rename content/{media => articles/2008-05-07-the-15-best-new-features-of-aperture-2}/aperture15_5.png (100%)
rename content/{media => articles/2008-05-07-the-15-best-new-features-of-aperture-2}/aperture15_6.png (100%)
rename content/{media => articles/2008-05-07-the-15-best-new-features-of-aperture-2}/aperture15_7.png (100%)
rename content/{media => articles/2008-05-07-the-15-best-new-features-of-aperture-2}/aperture15_8.jpg (100%)
rename content/{media => articles/2008-05-07-the-15-best-new-features-of-aperture-2}/aperture15_8.png (100%)
rename content/articles/{2008-05-07-the-15-best-new-features-of-aperture-2.md => 2008-05-07-the-15-best-new-features-of-aperture-2/index.md} (85%)
rename content/{media => articles/2008-05-07-the-15-best-new-features-of-aperture-2}/raw20.png (100%)
rename content/articles/{2008-05-08-the-iconfactory-presents-beautiful-indiana-jones-desktop-icons.md => 2008-05-08-the-iconfactory-presents-beautiful-indiana-jones-desktop-icons/index.md} (57%)
rename content/{media => articles/2008-05-08-the-iconfactory-presents-beautiful-indiana-jones-desktop-icons}/indianajones_dock.png (100%)
rename content/{media => articles/2008-05-08-the-iconfactory-presents-beautiful-indiana-jones-desktop-icons}/indianajones_first.png (100%)
rename content/articles/{2008-05-14-canon-updates-its-photographic-software-to-work-with-leopard.md => 2008-05-14-canon-updates-its-photographic-software-to-work-with-leopard/index.md} (100%)
create mode 100644 content/articles/2008-05-18-first-aperture-adjustment-plugins-have-arrived/aperture97.png
rename content/{media => articles/2008-05-18-first-aperture-adjustment-plugins-have-arrived}/apertureedit_interface.jpg (100%)
rename content/{media => articles/2008-05-18-first-aperture-adjustment-plugins-have-arrived}/creaceed_hydra.png (100%)
rename content/{media => articles/2008-05-18-first-aperture-adjustment-plugins-have-arrived}/dft-light_ui.jpg (100%)
rename content/{media => articles/2008-05-18-first-aperture-adjustment-plugins-have-arrived}/dft-ozone_ui.jpg (100%)
rename content/{media => articles/2008-05-18-first-aperture-adjustment-plugins-have-arrived}/dft-powerstroke_uis.jpg (100%)
rename content/{media => articles/2008-05-18-first-aperture-adjustment-plugins-have-arrived}/dpmatte_index1.jpg (100%)
rename content/{media => articles/2008-05-18-first-aperture-adjustment-plugins-have-arrived}/imagestrends_shieoff.jpg (100%)
rename content/{media => articles/2008-05-18-first-aperture-adjustment-plugins-have-arrived}/imagetrends_hemi_10.jpg (100%)
rename content/{media => articles/2008-05-18-first-aperture-adjustment-plugins-have-arrived}/imagetrends_pearlywhite.png (100%)
rename content/articles/{2008-05-18-first-aperture-adjustment-plugins-have-arrived.md => 2008-05-18-first-aperture-adjustment-plugins-have-arrived/index.md} (80%)
rename content/{media => articles/2008-05-18-first-aperture-adjustment-plugins-have-arrived}/lensfix_ci.png (100%)
rename content/{media => articles/2008-05-18-first-aperture-adjustment-plugins-have-arrived}/noiseninja.png (100%)
rename content/{media => articles/2008-05-18-first-aperture-adjustment-plugins-have-arrived}/tiffen_aperture.jpg (100%)
rename content/{media => articles/2008-05-18-first-aperture-adjustment-plugins-have-arrived}/viveza_aperture_ui.jpg (100%)
create mode 100644 content/articles/2008-05-19-image-trends-releases-fisheye-hemi-plug-in-for-aperture/imagetrends_hemi_10.jpg
rename content/articles/{2008-05-19-image-trends-releases-fisheye-hemi-plug-in-for-aperture.md => 2008-05-19-image-trends-releases-fisheye-hemi-plug-in-for-aperture/index.md} (74%)
rename content/articles/{2008-05-21-awesome-parallax-optical-illusion.md => 2008-05-21-awesome-parallax-optical-illusion/index.md} (57%)
rename content/{media => articles/2008-05-21-awesome-parallax-optical-illusion}/parallax_illusion_css.png (100%)
rename content/articles/{2008-05-22-showcasing-the-css-parallax-effect-12-creative-usages.md => 2008-05-22-showcasing-the-css-parallax-effect-12-creative-usages/index.md} (82%)
rename content/{media => articles/2008-05-22-showcasing-the-css-parallax-effect-12-creative-usages}/parallax-visualization.png (100%)
rename content/{media => articles/2008-05-22-showcasing-the-css-parallax-effect-12-creative-usages}/parallax-visualization_big.png (100%)
rename content/{media => articles/2008-05-22-showcasing-the-css-parallax-effect-12-creative-usages}/parallax_001.jpg (100%)
rename content/{media => articles/2008-05-22-showcasing-the-css-parallax-effect-12-creative-usages}/parallax_002.jpg (100%)
rename content/{media => articles/2008-05-22-showcasing-the-css-parallax-effect-12-creative-usages}/parallax_003.jpg (100%)
rename content/{media => articles/2008-05-22-showcasing-the-css-parallax-effect-12-creative-usages}/parallax_004.jpg (100%)
rename content/{media => articles/2008-05-22-showcasing-the-css-parallax-effect-12-creative-usages}/parallax_005.jpg (100%)
rename content/{media => articles/2008-05-22-showcasing-the-css-parallax-effect-12-creative-usages}/parallax_006.jpg (100%)
rename content/{media => articles/2008-05-22-showcasing-the-css-parallax-effect-12-creative-usages}/parallax_007.jpg (100%)
rename content/{media => articles/2008-05-22-showcasing-the-css-parallax-effect-12-creative-usages}/parallax_008.jpg (100%)
rename content/{media => articles/2008-05-22-showcasing-the-css-parallax-effect-12-creative-usages}/parallax_009.jpg (100%)
rename content/{media => articles/2008-05-22-showcasing-the-css-parallax-effect-12-creative-usages}/parallax_010.jpg (100%)
rename content/{media => articles/2008-05-22-showcasing-the-css-parallax-effect-12-creative-usages}/parallax_011.jpg (100%)
rename content/{media => articles/2008-05-22-showcasing-the-css-parallax-effect-12-creative-usages}/parallax_012.jpg (100%)
rename content/{media => articles/2008-05-22-showcasing-the-css-parallax-effect-12-creative-usages}/parallax_013.jpg (100%)
rename content/{media => articles/2008-05-22-showcasing-the-css-parallax-effect-12-creative-usages}/parallax_014.png (100%)
rename content/{media => articles/2008-05-22-showcasing-the-css-parallax-effect-12-creative-usages}/parallax_015.png (100%)
rename content/{media => articles/2008-05-22-showcasing-the-css-parallax-effect-12-creative-usages}/parallax_016.jpg (100%)
rename content/{media => articles/2008-05-22-showcasing-the-css-parallax-effect-12-creative-usages}/parallax_017.jpg (100%)
rename content/{media => articles/2008-05-28-new-all-in-one-aperture-plug-in-released}/apertureedit_logo.png (100%)
rename content/articles/{2008-05-28-new-all-in-one-aperture-plug-in-released.md => 2008-05-28-new-all-in-one-aperture-plug-in-released/index.md} (69%)
delete mode 100644 content/articles/2008-06-01-chives.md
rename content/{media => articles/2008-06-01-chives}/Teaser-Chives.jpg (100%)
rename content/{media => articles/2008-06-01-chives}/chives_by_kremalicious.zip (100%)
create mode 100644 content/articles/2008-06-01-chives/index.md
rename content/articles/{2008-06-02-tweet-im-on-twitter-now.md => 2008-06-02-tweet-im-on-twitter-now/index.md} (71%)
rename content/{media => articles/2008-06-02-tweet-im-on-twitter-now}/twitter-bird-kremalicious.png (100%)
rename content/{media => articles/2008-06-02-tweet-im-on-twitter-now}/twitter.png (100%)
rename content/{media => articles/2008-06-03-niepces-camera-obscura-and-the-history-of-the-first-photograph}/Teaser-Camera-Obscura-Icons.jpg (100%)
rename content/{media => articles/2008-06-03-niepces-camera-obscura-and-the-history-of-the-first-photograph}/View-from-the-Window-at-Le-Gras.png (100%)
rename content/{media => articles/2008-06-03-niepces-camera-obscura-and-the-history-of-the-first-photograph}/cameraobscura11_all.png (100%)
rename content/{media => articles/2008-06-03-niepces-camera-obscura-and-the-history-of-the-first-photograph}/cameraobscura_inuse.png (100%)
rename content/{media => articles/2008-06-03-niepces-camera-obscura-and-the-history-of-the-first-photograph}/coffee-cup-empty.png (100%)
rename content/articles/{2008-06-03-niepces-camera-obscura-and-the-history-of-the-first-photograph.md => 2008-06-03-niepces-camera-obscura-and-the-history-of-the-first-photograph/index.md} (90%)
rename content/{media => articles/2008-06-03-niepces-camera-obscura-and-the-history-of-the-first-photograph}/nicephore-niepce.jpg (100%)
rename content/{media => articles/2008-06-03-niepces-camera-obscura-and-the-history-of-the-first-photograph}/niepces_camera_obscura_by_kremalicious.zip (100%)
rename content/{media => articles/2008-06-03-niepces-camera-obscura-and-the-history-of-the-first-photograph}/view_from_the_window_at_le_gras_nicephore_niepce.jpg (100%)
rename content/{media => articles/2008-06-03-niepces-camera-obscura-wallpaper-pack}/Teaser-Camera-Obscura-Walls.jpg (100%)
rename content/articles/{2008-06-03-niepces-camera-obscura-wallpaper-pack.md => 2008-06-03-niepces-camera-obscura-wallpaper-pack/index.md} (77%)
rename content/{media => articles/2008-06-03-niepces-camera-obscura-wallpaper-pack}/niepces_camera_obscura_wallpaper_pack_by_kremalicious.zip (100%)
rename content/articles/{2008-06-11-safari-4-developer-preview.md => 2008-06-11-safari-4-developer-preview/index.md} (87%)
rename content/{media => articles/2008-06-11-safari-4-developer-preview}/safari-logo.png (100%)
rename content/{media => articles/2008-06-11-safari-4-developer-preview}/safari4_zoom.png (100%)
rename content/{media => articles/2008-06-11-safari-4-developer-preview}/safari_4_tabs.png (100%)
rename content/{media => articles/2008-06-11-safari-4-developer-preview}/webinspector_2.png (100%)
rename content/{media => articles/2008-06-11-safari-4-developer-preview}/webinspector_3.png (100%)
rename content/{media => articles/2008-06-17-quick-tip-borders-titles-plug-in-for-aperture}/aperture128.png (100%)
rename content/{media => articles/2008-06-17-quick-tip-borders-titles-plug-in-for-aperture}/aperture_bt.png (100%)
rename content/articles/{2008-06-17-quick-tip-borders-titles-plug-in-for-aperture.md => 2008-06-17-quick-tip-borders-titles-plug-in-for-aperture/index.md} (74%)
rename content/{media => articles/2008-06-19-ubuntu-as-mac-file-server-and-time-machine-volume}/bonjour97.png (100%)
create mode 100644 content/articles/2008-06-19-ubuntu-as-mac-file-server-and-time-machine-volume/coffee-cup-empty.png
rename content/articles/{2008-06-19-ubuntu-as-mac-file-server-and-time-machine-volume.md => 2008-06-19-ubuntu-as-mac-file-server-and-time-machine-volume/index.md} (87%)
rename content/{media => articles/2008-06-19-ubuntu-as-mac-file-server-and-time-machine-volume}/netatalk.png (100%)
rename content/{media => articles/2008-06-19-ubuntu-as-mac-file-server-and-time-machine-volume}/server_displays_by_kremalicious.zip (100%)
rename content/{media => articles/2008-06-19-ubuntu-as-mac-file-server-and-time-machine-volume}/serverdisplays.jpg (100%)
rename content/{media => articles/2008-06-19-ubuntu-as-mac-file-server-and-time-machine-volume}/timemachine97.png (100%)
rename content/{media => articles/2008-06-19-ubuntu-as-mac-file-server-and-time-machine-volume}/timemachinedisk97.png (100%)
rename content/{media => articles/2008-06-19-ubuntu-as-mac-file-server-and-time-machine-volume}/tm-sparsebundle.png (100%)
rename content/{media => articles/2008-06-19-ubuntu-as-mac-file-server-and-time-machine-volume}/ubuntu_mac_feature.jpg (100%)
rename content/{media => articles/2008-06-19-ubuntu-as-mac-file-server-and-time-machine-volume}/ubuntuserver1.png (100%)
rename content/{media => articles/2008-06-19-ubuntu-as-mac-file-server-and-time-machine-volume}/ubuntuserver2.png (100%)
rename content/{media => articles/2008-06-19-ubuntu-as-mac-file-server-and-time-machine-volume}/ubuntuserver3.png (100%)
rename content/{media => articles/2008-06-19-ubuntu-as-mac-file-server-and-time-machine-volume}/ubuntuserver4.png (100%)
rename content/{media => articles/2008-06-19-ubuntu-as-mac-file-server-and-time-machine-volume}/ubuntuserver4a.png (100%)
rename content/{media => articles/2008-06-19-ubuntu-as-mac-file-server-and-time-machine-volume}/ubuntuserver5.png (100%)
rename content/{media => articles/2008-06-19-ubuntu-as-mac-file-server-and-time-machine-volume}/ubuntuserver97.png (100%)
rename content/{media => articles/2008-06-25-apertureedit-is-edit-for-aperture-now-updated-to-12}/apertureedit_logo2.png (100%)
rename content/articles/{2008-06-25-apertureedit-is-edit-for-aperture-now-updated-to-12.md => 2008-06-25-apertureedit-is-edit-for-aperture-now-updated-to-12/index.md} (69%)
delete mode 100644 content/articles/2008-06-25-new-canon-stuff-xs-eos-1000-430ex-ii.md
rename content/{media => articles/2008-06-25-new-canon-stuff-xs-eos-1000-430ex-ii}/canon1000d.png (100%)
create mode 100644 content/articles/2008-06-25-new-canon-stuff-xs-eos-1000-430ex-ii/index.md
rename content/{media => articles/2008-06-25-new-canon-stuff-xs-eos-1000-430ex-ii}/speedlite430exii.jpg (100%)
delete mode 100644 content/articles/2008-07-01-everything-back-to-normal-on-kremaliciouscom-almost.md
rename content/{media => articles/2008-07-08-new-aperture-plug-ins-jade-and-ptlens}/aperture-plugin128.png (100%)
rename content/articles/{2008-07-08-new-aperture-plug-ins-jade-and-ptlens.md => 2008-07-08-new-aperture-plug-ins-jade-and-ptlens/index.md} (93%)
rename content/{media => articles/2008-07-08-new-aperture-plug-ins-jade-and-ptlens}/jade_ui.png (100%)
rename content/{media => articles/2008-07-08-new-aperture-plug-ins-jade-and-ptlens}/ptlens_ui.png (100%)
rename content/articles/{2008-07-11-enjoy-kremaliciousiphone.md => 2008-07-11-enjoy-kremaliciousiphone/index.md} (85%)
rename content/{media => articles/2008-07-11-enjoy-kremaliciousiphone}/kremalicious-iconiphone.png (100%)
rename content/{media => articles/2008-07-11-enjoy-kremaliciousiphone}/kremaliciousiphone.png (100%)
delete mode 100644 content/articles/2008-07-11-new-aperture-plug-in-nik-announces-silver-efex-pro.md
create mode 100644 content/articles/2008-07-11-new-aperture-plug-in-nik-announces-silver-efex-pro/aperture-plugin128.png
create mode 100644 content/articles/2008-07-11-new-aperture-plug-in-nik-announces-silver-efex-pro/index.md
rename content/{media => articles/2008-07-11-new-aperture-plug-in-nik-announces-silver-efex-pro}/nik_silverefex.png (100%)
rename content/articles/{2008-07-15-wordpress-25-get-rid-of-that-sluggish-dashboard.md => 2008-07-15-wordpress-25-get-rid-of-that-sluggish-dashboard/index.md} (72%)
rename content/{media => articles/2008-07-15-wordpress-25-get-rid-of-that-sluggish-dashboard}/wordpress-logo.png (100%)
rename content/articles/{2008-08-03-tips-for-journalists-reporting-from-china.md => 2008-08-03-tips-for-journalists-reporting-from-china/index.md} (98%)
rename content/{media => articles/2008-08-03-tips-for-journalists-reporting-from-china}/jingjing_chacha_kremalicious.jpg (100%)
rename content/articles/{2008-08-22-howto-create-a-mobile-encrypted-aperture-vault.md => 2008-08-22-howto-create-a-mobile-encrypted-aperture-vault/index.md} (75%)
rename content/{media => articles/2008-08-22-howto-create-a-mobile-encrypted-aperture-vault}/niepces_aperture_vault256.png (100%)
rename content/{media => articles/2008-08-22-howto-create-a-mobile-encrypted-aperture-vault}/securevault1.png (100%)
rename content/{media => articles/2008-08-22-howto-create-a-mobile-encrypted-aperture-vault}/securevault2.png (100%)
rename content/{media => articles/2008-08-22-howto-create-a-mobile-encrypted-aperture-vault}/securevault3.png (100%)
rename content/{media => articles/2008-08-22-howto-create-a-mobile-encrypted-aperture-vault}/securevault4.png (100%)
rename content/{media => articles/2008-08-22-howto-create-a-mobile-encrypted-aperture-vault}/securevault5.png (100%)
create mode 100644 content/articles/2008-08-22-noise-ninja-finally-available-for-apple-aperture/aperture-plugin128.png
rename content/articles/{2008-08-22-noise-ninja-finally-available-for-apple-aperture.md => 2008-08-22-noise-ninja-finally-available-for-apple-aperture/index.md} (60%)
rename content/articles/{2008-08-22-the-kremalicious-marsedit-style.md => 2008-08-22-the-kremalicious-marsedit-style/index.md} (75%)
rename content/{media => articles/2008-08-22-the-kremalicious-marsedit-style}/marsedit_kremalicious.png (100%)
rename content/{media => articles/2008-08-22-the-kremalicious-marsedit-style}/marsedit_kremalicious.txt (100%)
rename content/{media => articles/2008-08-22-the-kremalicious-marsedit-style}/marsedit_kremalicious_big.png (100%)
rename content/{media => articles/2008-08-26-icy-box-icons}/Teaser-Icy-Box.jpg (100%)
rename content/{media/icybox_teaser2.jpg => articles/2008-08-26-icy-box-icons/icybox.jpg} (100%)
rename content/{media => articles/2008-08-26-icy-box-icons}/icybox_by_kremalicious.zip (100%)
rename content/articles/{2008-08-26-icy-box-icons.md => 2008-08-26-icy-box-icons/index.md} (78%)
rename content/{media => articles/2008-08-28-architect-and-facade-theming-for-leopard}/architect-icon.jpg (100%)
rename content/{media => articles/2008-08-28-architect-and-facade-theming-for-leopard}/architect-ui.png (100%)
rename content/articles/{2008-08-28-architect-and-facade-theming-for-leopard.md => 2008-08-28-architect-and-facade-theming-for-leopard/index.md} (76%)
rename content/{media => articles/2008-08-28-architect-and-facade-theming-for-leopard}/magnifique-ui.png (100%)
delete mode 100644 content/articles/2008-08-28-canon-eos-50d-new-lens-announced.md
rename content/{media => articles/2008-08-28-canon-eos-50d-new-lens-announced}/efs_18-200.png (100%)
rename content/{media => articles/2008-08-28-canon-eos-50d-new-lens-announced}/eos_50D_back.png (100%)
rename content/{media => articles/2008-08-28-canon-eos-50d-new-lens-announced}/eos_50D_front.png (100%)
create mode 100644 content/articles/2008-08-28-canon-eos-50d-new-lens-announced/index.md
rename content/{media => articles/2008-09-01-a-new-browser-is-coming-google-chrome}/chrome-ui.png (100%)
rename content/{media => articles/2008-09-01-a-new-browser-is-coming-google-chrome}/googlechrome.png (100%)
rename content/articles/{2008-09-01-a-new-browser-is-coming-google-chrome.md => 2008-09-01-a-new-browser-is-coming-google-chrome/index.md} (60%)
rename content/{media => articles/2008-09-23-futurama-mars-university-wallpaper}/Teaser-Mars-U.jpg (100%)
rename content/articles/{2008-09-23-futurama-mars-university-wallpaper.md => 2008-09-23-futurama-mars-university-wallpaper/index.md} (66%)
rename content/{media => articles/2008-09-23-futurama-mars-university-wallpaper}/mars-u-wall-by-kremalicious.zip (100%)
rename content/{media => articles/2008-10-23-coffee-cup-icon}/Teaser-Coffee-Cup-Icon.jpg (100%)
rename content/{media => articles/2008-10-23-coffee-cup-icon}/coffee_cup_by_kremalicious.zip (100%)
rename content/articles/{2008-10-23-coffee-cup-icon.md => 2008-10-23-coffee-cup-icon/index.md} (75%)
create mode 100644 content/articles/2008-10-23-the-finest-coffee-cups-most-incredible-coffee-icons-on-the-web/Teaser-Coffee-Cup-Icon.jpg
rename content/{media => articles/2008-10-23-the-finest-coffee-cups-most-incredible-coffee-icons-on-the-web}/coffee-showcase-apple.png (100%)
rename content/{media => articles/2008-10-23-the-finest-coffee-cups-most-incredible-coffee-icons-on-the-web}/coffee-showcase-baumann.png (100%)
rename content/{media => articles/2008-10-23-the-finest-coffee-cups-most-incredible-coffee-icons-on-the-web}/coffee-showcase-benedik.png (100%)
rename content/{media => articles/2008-10-23-the-finest-coffee-cups-most-incredible-coffee-icons-on-the-web}/coffee-showcase-brasgalla.png (100%)
rename content/{media => articles/2008-10-23-the-finest-coffee-cups-most-incredible-coffee-icons-on-the-web}/coffee-showcase-brasgalla2.png (100%)
rename content/{media => articles/2008-10-23-the-finest-coffee-cups-most-incredible-coffee-icons-on-the-web}/coffee-showcase-cappuccinosofa.png (100%)
rename content/{media => articles/2008-10-23-the-finest-coffee-cups-most-incredible-coffee-icons-on-the-web}/coffee-showcase-england.png (100%)
rename content/{media => articles/2008-10-23-the-finest-coffee-cups-most-incredible-coffee-icons-on-the-web}/coffee-showcase-flarup.png (100%)
rename content/{media => articles/2008-10-23-the-finest-coffee-cups-most-incredible-coffee-icons-on-the-web}/coffee-showcase-jaeppinen.png (100%)
rename content/{media => articles/2008-10-23-the-finest-coffee-cups-most-incredible-coffee-icons-on-the-web}/coffee-showcase-kaycaffeine.png (100%)
rename content/{media => articles/2008-10-23-the-finest-coffee-cups-most-incredible-coffee-icons-on-the-web}/coffee-showcase-kretschmann.png (100%)
rename content/{media => articles/2008-10-23-the-finest-coffee-cups-most-incredible-coffee-icons-on-the-web}/coffee-showcase-lanham.png (100%)
rename content/{media => articles/2008-10-23-the-finest-coffee-cups-most-incredible-coffee-icons-on-the-web}/coffee-showcase-lopezruiz.png (100%)
rename content/{media => articles/2008-10-23-the-finest-coffee-cups-most-incredible-coffee-icons-on-the-web}/coffee-showcase-lovecappu.png (100%)
rename content/{media => articles/2008-10-23-the-finest-coffee-cups-most-incredible-coffee-icons-on-the-web}/coffee-showcase-macrabbit.png (100%)
rename content/{media => articles/2008-10-23-the-finest-coffee-cups-most-incredible-coffee-icons-on-the-web}/coffee-showcase-matu.png (100%)
rename content/{media => articles/2008-10-23-the-finest-coffee-cups-most-incredible-coffee-icons-on-the-web}/coffee-showcase-rask1.png (100%)
rename content/{media => articles/2008-10-23-the-finest-coffee-cups-most-incredible-coffee-icons-on-the-web}/coffee-showcase-rask2.png (100%)
rename content/{media => articles/2008-10-23-the-finest-coffee-cups-most-incredible-coffee-icons-on-the-web}/coffee-showcase-susumo.png (100%)
rename content/{media => articles/2008-10-23-the-finest-coffee-cups-most-incredible-coffee-icons-on-the-web}/coffee-showcase-times.png (100%)
rename content/{media => articles/2008-10-23-the-finest-coffee-cups-most-incredible-coffee-icons-on-the-web}/coffee-showcase-tut-abduzeedo.png (100%)
rename content/{media => articles/2008-10-23-the-finest-coffee-cups-most-incredible-coffee-icons-on-the-web}/coffee-showcase-tut-houle.png (100%)
rename content/{media => articles/2008-10-23-the-finest-coffee-cups-most-incredible-coffee-icons-on-the-web}/coffee-showcase-tut-psdtuts.png (100%)
rename content/{media => articles/2008-10-23-the-finest-coffee-cups-most-incredible-coffee-icons-on-the-web}/coffee-showcase-tut-vectuts.png (100%)
rename content/{media => articles/2008-10-23-the-finest-coffee-cups-most-incredible-coffee-icons-on-the-web}/coffee-showcase-vegrafik.png (100%)
rename content/{media => articles/2008-10-23-the-finest-coffee-cups-most-incredible-coffee-icons-on-the-web}/coffee-showcase-visualpharm.png (100%)
rename content/articles/{2008-10-23-the-finest-coffee-cups-most-incredible-coffee-icons-on-the-web.md => 2008-10-23-the-finest-coffee-cups-most-incredible-coffee-icons-on-the-web/index.md} (67%)
rename content/{media => articles/2008-10-26-the-definite-guide-to-watermarks-in-apple-aperture}/aperture-impression.png (100%)
rename content/{media => articles/2008-10-26-the-definite-guide-to-watermarks-in-apple-aperture}/aperture_borderfx.png (100%)
create mode 100644 content/articles/2008-10-26-the-definite-guide-to-watermarks-in-apple-aperture/aperture_bt.png
rename content/articles/{2008-10-26-the-definite-guide-to-watermarks-in-apple-aperture.md => 2008-10-26-the-definite-guide-to-watermarks-in-apple-aperture/index.md} (89%)
create mode 100644 content/articles/2008-10-26-the-definite-guide-to-watermarks-in-apple-aperture/watermark_5.png
create mode 100644 content/articles/2008-10-26-the-definite-guide-to-watermarks-in-apple-aperture/watermark_8.png
create mode 100644 content/articles/2008-10-26-the-definite-guide-to-watermarks-in-apple-aperture/watermark_aperture.jpg
rename content/{media => articles/2008-12-11-how-to-set-a-custom-gravatar-image-in-wordpress-27}/custom-gravatar.jpg (100%)
rename content/articles/{2008-12-11-how-to-set-a-custom-gravatar-image-in-wordpress-27.md => 2008-12-11-how-to-set-a-custom-gravatar-image-in-wordpress-27/index.md} (80%)
rename content/articles/{2008-12-13-howto-styling-author-comments-with-wordpress-27.md => 2008-12-13-howto-styling-author-comments-with-wordpress-27/index.md} (75%)
rename content/{media => articles/2009-01-05-the-mac-in-futurama}/futurama-mac-01.png (100%)
rename content/{media => articles/2009-01-05-the-mac-in-futurama}/futurama-mac-02.png (100%)
rename content/{media => articles/2009-01-05-the-mac-in-futurama}/futurama-mac-03.png (100%)
rename content/{media => articles/2009-01-05-the-mac-in-futurama}/futurama-mac-04.png (100%)
rename content/{media => articles/2009-01-05-the-mac-in-futurama}/futurama-mac-05.png (100%)
rename content/{media => articles/2009-01-05-the-mac-in-futurama}/futurama-mac-06.png (100%)
rename content/{media => articles/2009-01-05-the-mac-in-futurama}/futurama-mac-07.png (100%)
rename content/{media => articles/2009-01-05-the-mac-in-futurama}/futurama-mac-08.png (100%)
rename content/{media => articles/2009-01-05-the-mac-in-futurama}/futurama-mac-09.png (100%)
rename content/{media => articles/2009-01-05-the-mac-in-futurama}/futurama-mac-10.png (100%)
rename content/{media => articles/2009-01-05-the-mac-in-futurama}/futurama-mac-11.png (100%)
rename content/{media => articles/2009-01-05-the-mac-in-futurama}/futurama-mac-12.png (100%)
rename content/{media => articles/2009-01-05-the-mac-in-futurama}/futurama-mac-13.png (100%)
rename content/{media => articles/2009-01-05-the-mac-in-futurama}/futurama-mac-14.png (100%)
rename content/{media => articles/2009-01-05-the-mac-in-futurama}/futurama-mac-15.png (100%)
rename content/{media => articles/2009-01-05-the-mac-in-futurama}/futurama-mac-16.png (100%)
rename content/{media => articles/2009-01-05-the-mac-in-futurama}/futurama-mac-17.png (100%)
rename content/{media => articles/2009-01-05-the-mac-in-futurama}/futurama-mac-18.png (100%)
rename content/{media => articles/2009-01-05-the-mac-in-futurama}/futurama-mac-19.png (100%)
rename content/{media => articles/2009-01-05-the-mac-in-futurama}/futurama-mac-20.png (100%)
rename content/{media => articles/2009-01-05-the-mac-in-futurama}/futurama-mac-21.png (100%)
rename content/{media => articles/2009-01-05-the-mac-in-futurama}/futurama-mac-teaser.png (100%)
rename content/{media => articles/2009-01-05-the-mac-in-futurama}/futurama_gedeon.png (100%)
rename content/{media => articles/2009-01-05-the-mac-in-futurama}/futurama_hawkins1.png (100%)
rename content/{media => articles/2009-01-05-the-mac-in-futurama}/futurama_hawkins2.png (100%)
rename content/{media => articles/2009-01-05-the-mac-in-futurama}/futurama_kremalicious.png (100%)
rename content/{media => articles/2009-01-05-the-mac-in-futurama}/futurama_rawpixels.png (100%)
rename content/articles/{2009-01-05-the-mac-in-futurama.md => 2009-01-05-the-mac-in-futurama/index.md} (64%)
rename content/{media => articles/2009-01-05-the-mac-in-futurama}/out_of_whale_oil_detail.png (100%)
rename content/articles/{2009-02-01-portal-thingy.md => 2009-02-01-portal-thingy/index.md} (88%)
rename content/{media => articles/2009-02-01-portal-thingy}/vcardsite-arefjdey.png (100%)
rename content/{media => articles/2009-02-01-portal-thingy}/vcardsite-laurent.png (100%)
rename content/{media => articles/2009-02-01-portal-thingy}/vcardsite-maximilian.png (100%)
rename content/{media => articles/2009-02-01-portal-thingy}/vcardsite-mk.png (100%)
rename content/{media => articles/2009-02-01-portal-thingy}/vcardsite-rogie.png (100%)
rename content/{media => articles/2009-02-01-portal-thingy}/vcardsite-tim.png (100%)
rename content/{media => articles/2009-02-17-out-of-whale-oil}/Teaser-Out-Of-Whale-Oil.jpg (100%)
rename content/articles/{2009-02-17-out-of-whale-oil.md => 2009-02-17-out-of-whale-oil/index.md} (67%)
rename content/{media => articles/2009-02-17-out-of-whale-oil}/out-of-whale-oil-overview.png (100%)
rename content/{media => articles/2009-02-17-out-of-whale-oil}/out-of-whale-oil-wall-by-kremalicious.zip (100%)
create mode 100644 content/articles/2009-02-17-out-of-whale-oil/out_of_whale_oil_detail.png
rename content/{media => articles/2009-03-29-ultimate-coda-wordpress-share-link-bonanza}/coda-clips-icon-files.zip (100%)
rename content/{media => articles/2009-03-29-ultimate-coda-wordpress-share-link-bonanza}/codaclips-hud.png (100%)
rename content/{media => articles/2009-03-29-ultimate-coda-wordpress-share-link-bonanza}/codaclips-icon128.png (100%)
rename content/{media => articles/2009-03-29-ultimate-coda-wordpress-share-link-bonanza}/codaclips-placeholder.png (100%)
rename content/{media => articles/2009-03-29-ultimate-coda-wordpress-share-link-bonanza}/codaclips-teaser.png (100%)
rename content/{media => articles/2009-03-29-ultimate-coda-wordpress-share-link-bonanza}/coffee-cup-icon-kremalicious.png (100%)
rename content/articles/{2009-03-29-ultimate-coda-wordpress-share-link-bonanza.md => 2009-03-29-ultimate-coda-wordpress-share-link-bonanza/index.md} (64%)
rename content/{media => articles/2009-03-29-ultimate-coda-wordpress-share-link-bonanza}/share-link-bonanza-coda-clips.zip (100%)
rename content/{media => articles/2009-03-29-ultimate-coda-wordpress-share-link-bonanza}/share-link-bonanza-html.zip (100%)
rename content/{media => articles/2009-03-29-ultimate-coda-wordpress-share-link-bonanza}/tutorial-icon.png (100%)
rename content/{media => articles/2009-06-04-twitter-crisp}/Teaser-Twitter-Crisp.jpg (100%)
rename content/articles/{2009-06-04-twitter-crisp.md => 2009-06-04-twitter-crisp/index.md} (73%)
rename content/{media => articles/2009-06-04-twitter-crisp}/twitter-crisp-by-kremalicious.zip (100%)
rename content/{media => articles/2009-06-24-adiumeetie}/Adiumeetie-Dock-Preview.png (100%)
rename content/{media => articles/2009-06-24-adiumeetie}/Adiumeetie-Teaser-AdiumIcon.png (100%)
rename content/{media => articles/2009-06-24-adiumeetie}/Adiumeetie-Teaser.jpg (100%)
rename content/{media => articles/2009-06-24-adiumeetie}/Teaser-Adiumeetie.jpg (100%)
rename content/{media => articles/2009-06-24-adiumeetie}/adiumeetie-by-kremalicious.zip (100%)
rename content/articles/{2009-06-24-adiumeetie.md => 2009-06-24-adiumeetie/index.md} (75%)
rename content/{media => articles/2009-09-05-delibar}/Teaser-Delibar-Icons.jpg (100%)
rename content/{media => articles/2009-09-05-delibar}/delibar-by-kremalicious.zip (100%)
rename content/articles/{2009-09-05-delibar.md => 2009-09-05-delibar/index.md} (78%)
rename content/articles/{2009-12-17-wordpress-post-thumbnails.md => 2009-12-17-wordpress-post-thumbnails/index.md} (68%)
rename content/{media => articles/2009-12-17-wordpress-post-thumbnails}/wordpress-thumbnail-1.png (100%)
rename content/{media => articles/2009-12-17-wordpress-post-thumbnails}/wordpress-thumbnail-2.png (100%)
rename content/{media => articles/2009-12-17-wordpress-post-thumbnails}/wordpress-thumbnail-3.png (100%)
rename content/{media => articles/2009-12-17-wordpress-post-thumbnails}/wordpress-thumbnail-4.png (100%)
rename content/{media => articles/2009-12-17-wordpress-post-thumbnails}/wordpress-thumbnail-5.png (100%)
rename content/{media => articles/2010-02-04-ipixelpad}/Teaser-iPixelPad.png (100%)
rename content/{media => articles/2010-02-04-ipixelpad}/iPixelPad-Teaser-kremalicious2.jpg (100%)
rename content/{media => articles/2010-02-04-ipixelpad}/iPixelPad-Teaser1.png (100%)
rename content/articles/{2010-02-04-ipixelpad.md => 2010-02-04-ipixelpad/index.md} (76%)
rename content/{media => articles/2010-02-04-ipixelpad}/ipixelpad-homescreen-zoom.png (100%)
rename content/{media => articles/2010-02-04-ipixelpad}/ipixelpad_by_kremalicious.zip (100%)
rename content/{media => articles/2010-07-03-momcorp}/MomCorp-Walls-Overview.png (100%)
rename content/{media => articles/2010-07-03-momcorp}/Teaser-MomCorp-Wall.png (100%)
rename content/articles/{2010-07-03-momcorp.md => 2010-07-03-momcorp/index.md} (74%)
rename content/{media => articles/2010-07-03-momcorp}/momcorp_wall_by_kremalicious.zip (100%)
rename content/{media/Badged-Teaser-kremalicious@2x.png => articles/2011-12-15-badged/Badged-Teaser-kremalicious.png} (100%)
rename content/{media => articles/2011-12-15-badged}/badged-settings.png (100%)
rename content/articles/{2011-12-15-badged.md => 2011-12-15-badged/index.md} (97%)
rename content/articles/{2012-02-26-mk-v2.md => 2012-02-26-mk-v2/index.md} (89%)
rename content/{media => articles/2012-02-26-mk-v2}/mkv2-android-chrome.png (100%)
rename content/{media => articles/2012-02-26-mk-v2}/mkv2-android-firefox.png (100%)
rename content/{media => articles/2012-02-26-mk-v2}/mkv2-android.png (100%)
rename content/{media => articles/2012-02-26-mk-v2}/mkv2-balls.jpg (100%)
rename content/{media => articles/2012-02-26-mk-v2}/mkv2-detail.png (100%)
rename content/{media => articles/2012-02-26-mk-v2}/mkv2-ipad-touchindicator.jpg (100%)
rename content/{media => articles/2012-02-26-mk-v2}/mkv2-ipad.png (100%)
rename content/{media => articles/2012-02-26-mk-v2}/mkv2-iphone.png (100%)
rename content/{media => articles/2012-02-26-mk-v2}/mkv2-portfolio-overlay.jpg (100%)
rename content/{media => articles/2012-02-26-mk-v2}/mkv2-responsivelayouts.jpg (100%)
rename content/{media => articles/2012-02-26-mk-v2}/mkv2-teaser-450x250.jpg (100%)
rename content/{media => articles/2012-02-26-mk-v2}/mkv2-teaser-500x277.jpg (100%)
rename content/{media => articles/2012-02-26-mk-v2}/mkv2-teaser-540x288.jpg (100%)
rename content/{media => articles/2012-02-26-mk-v2}/mkv2.jpg (100%)
rename content/{media => articles/2012-04-04-android-tab-conundrum}/Instagram-Swipe.png (100%)
rename content/{media => articles/2012-04-04-android-tab-conundrum}/android-galaxy-note.png (100%)
rename content/{media => articles/2012-04-04-android-tab-conundrum}/android-navigation-buttons.png (100%)
rename content/articles/{2012-04-04-android-tab-conundrum.md => 2012-04-04-android-tab-conundrum/index.md} (95%)
rename content/{media => articles/2012-04-04-android-tab-conundrum}/tabs_overview.png (100%)
rename content/articles/{2012-05-14-welcome-to-kremalicious2.md => 2012-05-14-welcome-to-kremalicious2/index.md} (92%)
rename content/{media => articles/2012-05-14-welcome-to-kremalicious2}/kremalicious2-photogrid.jpg (100%)
rename content/{media => articles/2012-05-14-welcome-to-kremalicious2}/kremalicious2-photoposts.jpg (100%)
rename content/{media => articles/2012-05-14-welcome-to-kremalicious2}/kremalicious2-teaser.jpg (100%)
rename content/{media => articles/2012-05-14-welcome-to-kremalicious2}/kremalicious2-topicicons.jpg (100%)
rename content/{media => articles/2012-05-14-welcome-to-kremalicious2}/kremalicious2-typography.jpg (100%)
rename content/{media => articles/2012-05-14-welcome-to-kremalicious2}/kremaliciouscom-iPad-3.jpg (100%)
rename content/{media => articles/2012-05-15-wp-icons-template}/WordPress-Admin-Icons-Template-Filled.png (100%)
rename content/articles/{2012-05-15-wp-icons-template.md => 2012-05-15-wp-icons-template/index.md} (98%)
rename content/{media => articles/2012-05-15-wp-icons-template}/kremalicious-Teaser-WP-Icon-Template.png (100%)
rename content/articles/{2012-06-13-retina-icons-in-wordpress-3-4.md => 2012-06-13-retina-icons-in-wordpress-3-4/index.md} (90%)
create mode 100644 content/articles/2012-06-13-retina-icons-in-wordpress-3-4/kremalicious-Teaser-WP-Icon-Template.png
rename content/{media => articles/2012-06-13-retina-icons-in-wordpress-3-4}/wp34_retina_icons.png (100%)
rename content/{media => articles/2012-07-15-add-your-web-site-to-the-windows-8-metro-ui}/Windows-8-Metro-tile-kremalicious-all-apps.png (100%)
rename content/{media => articles/2012-07-15-add-your-web-site-to-the-windows-8-metro-ui}/Windows-8-Metro-tile-kremalicious-in-action.png (100%)
rename content/{media => articles/2012-07-15-add-your-web-site-to-the-windows-8-metro-ui}/Windows-8-Metro-tile-kremalicious.png (100%)
rename content/articles/{2012-07-15-add-your-web-site-to-the-windows-8-metro-ui.md => 2012-07-15-add-your-web-site-to-the-windows-8-metro-ui/index.md} (89%)
rename content/{media => articles/2012-07-15-add-your-web-site-to-the-windows-8-metro-ui}/kremalicious-Teaser-Metro-Tile.jpg (100%)
delete mode 100644 content/articles/2012-07-16-using-kbd-for-fun-and-profit/Google Android License.txt
delete mode 100644 content/articles/2012-07-16-using-kbd-for-fun-and-profit/Roboto-Regular-webfont.eot
delete mode 100644 content/articles/2012-07-16-using-kbd-for-fun-and-profit/Roboto-Regular-webfont.svg
delete mode 100644 content/articles/2012-07-16-using-kbd-for-fun-and-profit/Roboto-Regular-webfont.ttf
delete mode 100644 content/articles/2012-07-16-using-kbd-for-fun-and-profit/Roboto-Regular-webfont.woff
create mode 100644 content/articles/2012-07-16-using-kbd-for-fun-and-profit/_post-kbd.css
delete mode 100644 content/articles/2012-07-16-using-kbd-for-fun-and-profit/post-kbd.css
rename content/{media => articles/2012-08-07-projectpurple}/Project-Purple-Dribbble.png (100%)
rename content/{media => articles/2012-08-07-projectpurple}/Teaser-Project-Purple.png (100%)
rename content/articles/{2012-08-07-projectpurple.md => 2012-08-07-projectpurple/index.md} (58%)
rename content/{media => articles/2012-08-07-projectpurple}/project-purple-ipad-kremalicious.png (100%)
rename content/{media => articles/2012-08-07-projectpurple}/project-purple-iphone4-kremalicious.png (100%)
rename content/{media => articles/2012-08-07-projectpurple}/project-purple-kremalicious.png (100%)
rename content/{media => articles/2012-08-07-projectpurple}/project-purple-kremalicious.zip (100%)
rename content/{media => articles/2012-08-07-projectpurple}/project-purple-nexus-kremalicious.png (100%)
rename content/articles/{2012-08-20-im-joining-ezeep.md => 2012-08-20-im-joining-ezeep/index.md} (94%)
rename content/{media => articles/2012-08-20-im-joining-ezeep}/kremalicious-Teaser-ezeep.png (100%)
rename content/{media => articles/2013-07-13-enterprise-software-sucks}/buddha-colorscheme.png (100%)
rename content/{media => articles/2013-07-13-enterprise-software-sucks}/buddha-printer.png (100%)
rename content/articles/{2013-07-13-enterprise-software-sucks.md => 2013-07-13-enterprise-software-sucks/index.md} (97%)
rename content/articles/{2013-08-07-stealing-time-how-technology-can-hurt-or-harm-our-inner-state.md => 2013-08-07-stealing-time-how-technology-can-hurt-or-harm-our-inner-state/index.md} (99%)
rename content/{media => articles/2013-08-07-stealing-time-how-technology-can-hurt-or-harm-our-inner-state}/post-time.png (100%)
rename content/articles/{2015-09-13-css-app-store-badges.md => 2015-09-13-css-app-store-badges/index.md} (92%)
rename content/{media => articles/2015-09-13-css-app-store-badges}/teaser-appstorebadges.png (100%)
create mode 100644 content/articles/2023-09-18-favicon-generation-with-astro/favicon-generation-with-astro-teaser.png
create mode 100644 content/articles/2023-09-18-favicon-generation-with-astro/index.md
create mode 100644 content/config.ts
delete mode 100644 content/media/Badged-Teaser-kremalicious.png
delete mode 100644 content/media/Delibar-Icons-Teaser.jpg
delete mode 100644 content/media/Delibar-Icons-Teaser2.png
delete mode 100644 content/media/Twitter-Crisp-16.png
delete mode 100644 content/media/adiumeetie-goodies-teaser.png
delete mode 100644 content/media/aperture72.png
delete mode 100644 content/media/box_download.png
delete mode 100644 content/media/camera_obscura_wall_teaser.png
delete mode 100644 content/media/cameraobscura11_all_thumb.png
delete mode 100644 content/media/cameraobscura_aperture_128.png
delete mode 100644 content/media/cameraobscura_teaser1.png
delete mode 100644 content/media/ce2d29f40df411e2ad5812313817873b_7.jpg
delete mode 100644 content/media/chives_wallpaper_teaser.png
delete mode 100644 content/media/codaclips-icon48.png
delete mode 100644 content/media/coffee-cup-icon-teaser.png
delete mode 100644 content/media/delibar-website.png
delete mode 100644 content/media/deliciouslinks.png
delete mode 100644 content/media/diskimage98.png
delete mode 100644 content/media/efs_18-200_thumb.png
delete mode 100644 content/media/elegancesociale_smashingteaser.png
delete mode 100644 content/media/encryption_certificate.png
delete mode 100644 content/media/encryption_mail2.png
delete mode 100644 content/media/eos_50D_back_thumb.png
delete mode 100644 content/media/eos_50D_front_thumb.png
delete mode 100644 content/media/filevault98.png
delete mode 100644 content/media/firefox-icon.png
delete mode 100644 content/media/firefox3promo.png
delete mode 100644 content/media/free-pdf.png
delete mode 100644 content/media/fry-2009-kremalicious.png
delete mode 100644 content/media/hastuzeit-kremalicious2.jpg
delete mode 100644 content/media/html-document-icon48.png
delete mode 100644 content/media/icon-Facebook.png
delete mode 100644 content/media/icon-Twitter2.png
delete mode 100644 content/media/icon-deviantART.png
delete mode 100644 content/media/icybox_teaser1.png
delete mode 100644 content/media/icybox_teaser2_small.png
delete mode 100644 content/media/jade_ui_thumb.jpg
delete mode 100644 content/media/kremalicious-iconiphone-80.png
delete mode 100644 content/media/kremalicious_nav.txt
delete mode 100644 content/media/kremaliciousiphone_thumb.png
delete mode 100644 content/media/lens.png
delete mode 100644 content/media/mars-u-teaser.png
delete mode 100644 content/media/mediaMomCorpTeaser.png
delete mode 100644 content/media/niepce_portrait.png
delete mode 100644 content/media/nik_silverefex_thumb.png
delete mode 100644 content/media/opera-icon.png
delete mode 100644 content/media/paypal-logo.jpg
delete mode 100644 content/media/ptlens_ui_thumb.jpg
delete mode 100644 content/media/republica-banner-125x160_black.png
delete mode 100644 content/media/republica10_kremaliciousbanner.png
delete mode 100644 content/media/safari4_zoom_thumb.png
delete mode 100644 content/media/snippet.png
delete mode 100644 content/media/softwareupdate.png
delete mode 100644 content/media/softwareupdate_photo200.png
delete mode 100644 content/media/teaser-out-of-whale-oil.png
delete mode 100644 content/media/teaser_elegance-sociale.png
delete mode 100644 content/media/tweetie_select_bubbles.zip
delete mode 100644 content/media/twitter-crisp-teaser.jpg
delete mode 100644 content/media/twitter-crisp-teaser2.png
delete mode 100644 content/media/webinspector_1.png
delete mode 100644 content/media/xserve_screwed.png
rename content/{media => photos/2005-07-26-beaucarnea-leafs}/img_1820-Version-4.jpg (100%)
rename content/photos/{2005-07-26-beaucarnea-leafs.md => 2005-07-26-beaucarnea-leafs/index.md} (84%)
rename content/photos/{2005-08-02-snail-on-a-leaf.md => 2005-08-02-snail-on-a-leaf/index.md} (86%)
rename content/{media => photos/2005-08-02-snail-on-a-leaf}/schnecke_blatt.jpg (100%)
rename content/{media => photos/2006-07-23-electricity}/MG_5885_2006-7-23.jpg (100%)
rename content/photos/{2006-07-23-electricity.md => 2006-07-23-electricity/index.md} (84%)
rename content/{media => photos/2006-07-23-floating-sky}/floating-sky-1.jpg (100%)
rename content/photos/{2006-07-23-floating-sky.md => 2006-07-23-floating-sky/index.md} (90%)
rename content/{media => photos/2006-08-21-new-berlin-bridge}/berliner_bruecke1-HDR-16bit.jpg (100%)
rename content/photos/{2006-08-21-new-berlin-bridge.md => 2006-08-21-new-berlin-bridge/index.md} (79%)
rename content/photos/{2006-09-16-wooden-windmill.md => 2006-09-16-wooden-windmill/index.md} (77%)
rename content/{media => photos/2006-09-16-wooden-windmill}/muehle_suhlendorf_HDR_Tonemapped_16bit.jpg (100%)
rename content/{media => photos/2006-10-22-german-chancellery}/bundeskanzleramt_01_HDR_tonemapped_16bit-Version-2.jpg (100%)
rename content/photos/{2006-10-22-german-chancellery.md => 2006-10-22-german-chancellery/index.md} (74%)
rename content/{media => photos/2007-02-10-macbook-abstract}/MG_9313_2007-02-10.jpg (100%)
rename content/photos/{2007-02-10-macbook-abstract.md => 2007-02-10-macbook-abstract/index.md} (82%)
rename content/{media => photos/2008-06-04-helvetica-typewriter-keys}/MG_1735-Version-2.jpg (100%)
rename content/photos/{2008-06-04-helvetica-typewriter-keys.md => 2008-06-04-helvetica-typewriter-keys/index.md} (86%)
rename content/photos/{2008-06-30-stone-head.md => 2008-06-30-stone-head/index.md} (89%)
rename content/{media => photos/2008-06-30-stone-head}/stonehead.jpg (100%)
rename content/{media => photos/2008-07-23-leaf-life}/MG_1920.jpg (100%)
rename content/photos/{2008-07-23-leaf-life.md => 2008-07-23-leaf-life/index.md} (87%)
rename content/{media => photos/2008-09-23-a-long-time-ago}/A-Long-Time-Ago.jpg (100%)
rename content/photos/{2008-09-23-a-long-time-ago.md => 2008-09-23-a-long-time-ago/index.md} (93%)
rename content/{media => photos/2010-03-27-office-desk}/Office-Desk.jpg (100%)
rename content/photos/{2010-03-27-office-desk.md => 2010-03-27-office-desk/index.md} (86%)
rename content/{media => photos/2010-07-18-typeface-condoms}/Typeface-condoms.jpg (100%)
rename content/photos/{2010-07-18-typeface-condoms.md => 2010-07-18-typeface-condoms/index.md} (88%)
rename content/{media => photos/2010-08-07-bonsai}/Bonsai-5-Version-2.jpg (100%)
rename content/photos/{2010-08-07-bonsai.md => 2010-08-07-bonsai/index.md} (81%)
rename content/{media => photos/2010-09-01-gdr-helvetica}/GDR-Helvetica.jpg (100%)
rename content/photos/{2010-09-01-gdr-helvetica.md => 2010-09-01-gdr-helvetica/index.md} (86%)
rename content/{media => photos/2010-12-11-iphone-coasters}/iPhone-Coasters-1-Version-2.jpg (100%)
rename content/photos/{2010-12-11-iphone-coasters.md => 2010-12-11-iphone-coasters/index.md} (86%)
rename content/{media => photos/2010-12-29-basically-the-monolith-is-on-my-desk}/Basically-The-Monolith-Is-On-My-Desk.jpg (100%)
rename content/photos/{2010-12-29-basically-the-monolith-is-on-my-desk.md => 2010-12-29-basically-the-monolith-is-on-my-desk/index.md} (84%)
rename content/{media => photos/2010-12-29-free-monkey-breath-not-soylent-green}/Free-Monkey-Breath-Not-Soylent-Green.jpg (100%)
rename content/photos/{2010-12-29-free-monkey-breath-not-soylent-green.md => 2010-12-29-free-monkey-breath-not-soylent-green/index.md} (81%)
rename content/{media => photos/2011-01-08-enjoying-paper}/Enjoying-Paper.jpg (100%)
rename content/photos/{2011-01-08-enjoying-paper.md => 2011-01-08-enjoying-paper/index.md} (86%)
rename content/{media => photos/2011-01-08-glowing-star-inside}/Glowing-Star-Inside.jpg (100%)
rename content/photos/{2011-01-08-glowing-star-inside.md => 2011-01-08-glowing-star-inside/index.md} (81%)
rename content/{media => photos/2011-01-18-historic-flood-levels}/Historic-Flood-Levels.jpg (100%)
rename content/photos/{2011-01-18-historic-flood-levels.md => 2011-01-18-historic-flood-levels/index.md} (82%)
rename content/{media => photos/2011-10-11-broken-nexus-s-screen}/Broken-Nexus-S-Screen.jpg (100%)
rename content/photos/{2011-10-11-broken-nexus-s-screen.md => 2011-10-11-broken-nexus-s-screen/index.md} (86%)
rename content/{media => photos/2012-03-04-relaxing-cat}/7f9397a265d811e1b9f1123138140926_7.jpg (100%)
rename content/photos/{2012-03-04-relaxing-cat.md => 2012-03-04-relaxing-cat/index.md} (66%)
rename content/{media => photos/2012-04-03-blaue-turme}/Blaue-Tuerme-1.jpg (100%)
rename content/photos/{2012-04-03-blaue-turme.md => 2012-04-03-blaue-turme/index.md} (86%)
rename content/{media => photos/2012-04-03-skeletor}/6313cc1e7db611e180c9123138016265_7.jpg (100%)
rename content/photos/{2012-04-03-skeletor.md => 2012-04-03-skeletor/index.md} (50%)
rename content/{media => photos/2012-04-03-train-station-leipzig}/de2ac24c7db911e1b9f1123138140926_7.jpg (100%)
rename content/photos/{2012-04-03-train-station-leipzig.md => 2012-04-03-train-station-leipzig/index.md} (55%)
rename content/{media => photos/2012-04-05-current-sushi-status}/aff38e2c7f5311e1b10e123138105d6b_7.jpg (100%)
rename content/photos/{2012-04-05-current-sushi-status.md => 2012-04-05-current-sushi-status/index.md} (55%)
delete mode 100644 content/photos/2012-04-07-buna.md
rename content/{media => photos/2012-04-07-buna}/44af28f2805b11e18cf91231380fd29b_7.jpg (100%)
create mode 100644 content/photos/2012-04-07-buna/index.md
rename content/{media => photos/2012-04-07-cat-enjoying-a-good-ipad-game}/7838011c80ce11e19e4a12313813ffc0_7.jpg (100%)
rename content/photos/{2012-04-07-cat-enjoying-a-good-ipad-game.md => 2012-04-07-cat-enjoying-a-good-ipad-game/index.md} (69%)
rename content/{media => photos/2012-04-07-ipad-porn}/97a44d6080b711e181bd12313817987b_7.jpg (100%)
rename content/photos/{2012-04-07-ipad-porn.md => 2012-04-07-ipad-porn/index.md} (60%)
rename content/{media => photos/2012-04-07-opera}/5df6e0a280c911e1a87612313804ec91_7.jpg (100%)
rename content/photos/{2012-04-07-opera.md => 2012-04-07-opera/index.md} (60%)
rename content/{media => photos/2012-04-08-common-kitchen-decoration}/2ba6eeba81b111e1989612313815112c_7.jpg (100%)
rename content/photos/{2012-04-08-common-kitchen-decoration.md => 2012-04-08-common-kitchen-decoration/index.md} (65%)
rename content/{media => photos/2012-04-08-graffiti-old-school-style}/7e2b28f881b711e1af7612313813f8e8_7.jpg (100%)
rename content/photos/{2012-04-08-graffiti-old-school-style.md => 2012-04-08-graffiti-old-school-style/index.md} (71%)
rename content/{media => photos/2012-05-03-antique-chrome}/5fc688aa953811e180c9123138016265_7.jpg (100%)
rename content/photos/{2012-05-03-antique-chrome.md => 2012-05-03-antique-chrome/index.md} (70%)
rename content/{media/41b5a454a43811e1989612313815112c_7.jpeg => photos/2012-05-23-like-modern-heating-only-more-badass/41b5a454a43811e1989612313815112c_7.jpg} (100%)
rename content/photos/{2012-05-23-like-modern-heating-only-more-badass.md => 2012-05-23-like-modern-heating-only-more-badass/index.md} (80%)
rename content/{media => photos/2012-05-27-balloon}/690fe368a81911e1b2fe1231380205bf_7.jpg (100%)
rename content/photos/{2012-05-27-balloon.md => 2012-05-27-balloon/index.md} (71%)
rename content/{media => photos/2012-06-27-typography-window}/80a136dabff711e188131231381b5c25_7.jpg (100%)
rename content/photos/{2012-06-27-typography-window.md => 2012-06-27-typography-window/index.md} (70%)
rename content/{media => photos/2012-07-20-mmmmh-coffee}/66a6e0c0d25a11e1a94522000a1e8aaf_7.jpg (100%)
rename content/photos/{2012-07-20-mmmmh-coffee.md => 2012-07-20-mmmmh-coffee/index.md} (66%)
rename content/{media => photos/2012-08-08-amazingly-early}/2ca7a094e10f11e1868c12313817a130_7.jpg (100%)
rename content/photos/{2012-08-08-amazingly-early.md => 2012-08-08-amazingly-early/index.md} (66%)
rename content/{media => photos/2012-08-25-so-much-room}/c0c45b6eeea211e1ad8e22000a1cdbb8_7.jpg (100%)
rename content/photos/{2012-08-25-so-much-room.md => 2012-08-25-so-much-room/index.md} (65%)
rename content/{media => photos/2012-09-07-huge-station-is-huge}/619b3900f92911e1a31922000a1cddf1_7.jpg (100%)
rename content/photos/{2012-09-07-huge-station-is-huge.md => 2012-09-07-huge-station-is-huge/index.md} (60%)
rename content/{media => photos/2012-09-10-subway-firefox}/84f9d2c4fb7411e19ca422000a1d0119_7.jpg (100%)
rename content/photos/{2012-09-10-subway-firefox.md => 2012-09-10-subway-firefox/index.md} (66%)
rename content/{media => photos/2012-09-12-sweet-typography}/01f8b0b8fcc611e19b5b123138140bce_7.jpg (100%)
rename content/photos/{2012-09-12-sweet-typography.md => 2012-09-12-sweet-typography/index.md} (70%)
rename content/{media => photos/2012-09-14-bvg-dos}/6c4003f2fe9911e1ae9122000a1e9e21_7.jpg (100%)
rename content/photos/{2012-09-14-bvg-dos.md => 2012-09-14-bvg-dos/index.md} (56%)
rename content/{media => photos/2013-02-17-castle-garden}/8372983659_da0e88ca79_o.jpg (100%)
rename content/photos/{2013-02-17-castle-garden.md => 2013-02-17-castle-garden/index.md} (78%)
rename content/{media => photos/2013-02-17-ezeep-android-app-design}/8455835942_a9b9100373_o.jpg (100%)
rename content/photos/{2013-02-17-ezeep-android-app-design.md => 2013-02-17-ezeep-android-app-design/index.md} (84%)
rename content/{media => photos/2013-02-17-ezeep-office-view}/8450618380_83c64006c6_o.jpg (100%)
rename content/photos/{2013-02-17-ezeep-office-view.md => 2013-02-17-ezeep-office-view/index.md} (83%)
rename content/{media => photos/2013-05-23-ezeep-birds}/8776417095_43553c88c2_o.jpg (100%)
rename content/photos/{2013-05-23-ezeep-birds.md => 2013-05-23-ezeep-birds/index.md} (75%)
rename content/{media => photos/2013-05-23-ezeep-origami}/8782995066_e90ff6b3ae_o.jpg (100%)
rename content/photos/{2013-05-23-ezeep-origami.md => 2013-05-23-ezeep-origami/index.md} (75%)
rename content/photos/{2014-03-10-just-a-normal-sunday.md => 2014-03-10-just-a-normal-sunday/index.md} (52%)
rename content/{media => photos/2014-03-10-just-a-normal-sunday}/just-a-normal-sunday.jpg (100%)
rename content/photos/{2014-03-15-potsdam.md => 2014-03-15-potsdam/index.md} (75%)
rename content/{media => photos/2014-03-15-potsdam}/potsdam.jpg (100%)
rename content/photos/{2014-03-17-typographic-diamond.md => 2014-03-17-typographic-diamond/index.md} (87%)
rename content/{media => photos/2014-03-17-typographic-diamond}/typographic-diamond.jpg (100%)
rename content/{media => photos/2014-04-26-ai-weiwei-stools}/ai-wei-wei-stools.jpg (100%)
rename content/photos/{2014-04-26-ai-weiwei-stools.md => 2014-04-26-ai-weiwei-stools/index.md} (87%)
rename content/{media => photos/2014-06-07-airfield-reference-point}/airfield-reference-point.jpg (100%)
rename content/photos/{2014-06-07-airfield-reference-point.md => 2014-06-07-airfield-reference-point/index.md} (85%)
rename content/photos/{ => 2015-03-29-anton-henning-heimat-schaffen-simpsons}/2015-03-29-anton-henning-heimat-schaffen-simpsons.jpg (100%)
rename content/photos/{2015-03-29-anton-henning-heimat-schaffen-simpsons.md => 2015-03-29-anton-henning-heimat-schaffen-simpsons/index.md} (77%)
rename content/photos/{ => 2015-03-29-thanks-for-the-tip-little-orange-blob}/2015-03-29-thanks-for-the-tip-little-orange-blob.jpg (100%)
rename content/photos/{2015-03-29-thanks-for-the-tip-little-orange-blob.md => 2015-03-29-thanks-for-the-tip-little-orange-blob/index.md} (67%)
rename content/photos/{2015-04-09-most-surprising-dog-i-know.md => 2015-04-09-most-surprising-dog-i-know/index.md} (72%)
rename content/{media => photos/2015-04-09-most-surprising-dog-i-know}/most-surprising-dog-i-know.jpg (100%)
rename content/photos/{2015-04-16-obligatory-it-s-summer-in-berlin-photo.md => 2015-04-16-obligatory-it-s-summer-in-berlin-photo/index.md} (69%)
rename content/{media => photos/2015-04-16-obligatory-it-s-summer-in-berlin-photo}/obligatory-it-s-summer-in-berlin-photo.jpg (100%)
rename content/photos/{ => 2015-04-17-suddenly-a-new-hindu-temple-appears-around-the-corner}/2015-04-17-suddenly-a-new-hindu-temple-appears-around-the-corner.jpg (100%)
rename content/photos/{2015-04-17-suddenly-a-new-hindu-temple-appears-around-the-corner.md => 2015-04-17-suddenly-a-new-hindu-temple-appears-around-the-corner/index.md} (66%)
rename content/{media => photos/2015-04-23-that-moment-when-your-childhood-toys-hang-painted-in-a-gallery}/Monstrum_Gameboy_Catherine_Kaleel.jpg (100%)
rename content/photos/{2015-04-23-that-moment-when-your-childhood-toys-hang-painted-in-a-gallery.md => 2015-04-23-that-moment-when-your-childhood-toys-hang-painted-in-a-gallery/index.md} (87%)
rename content/photos/{2015-04-25-tiny-tiny-demons.md => 2015-04-25-tiny-tiny-demons/index.md} (75%)
rename content/{media => photos/2015-04-25-tiny-tiny-demons}/tiny_tiny_demons.jpg (100%)
rename content/photos/{2016-02-25-gaudi-knows-how-to-impress-with-a-ceiling.md => 2016-02-25-gaudi-knows-how-to-impress-with-a-ceiling/index.md} (68%)
rename content/{media => photos/2016-02-25-gaudi-knows-how-to-impress-with-a-ceiling}/sagrada-familia-ceiling.jpg (100%)
rename content/photos/{2016-02-27-streets-of-el-raval.md => 2016-02-27-streets-of-el-raval/index.md} (80%)
rename content/{media => photos/2016-02-27-streets-of-el-raval}/streets-of-el-raval.jpg (100%)
rename content/{media => photos/2016-03-02-a-storm-is-coming}/a-storm-is-coming.jpg (100%)
rename content/photos/{2016-03-02-a-storm-is-coming.md => 2016-03-02-a-storm-is-coming/index.md} (62%)
rename content/photos/{2017-02-13-keith-haring-vandalizing-a-wall.md => 2017-02-13-keith-haring-vandalizing-a-wall/index.md} (80%)
rename content/{media => photos/2017-02-13-keith-haring-vandalizing-a-wall}/keith-haring-vandalizing-a-wall.jpg (100%)
rename content/photos/{2017-02-13-streets-of-el-born.md => 2017-02-13-streets-of-el-born/index.md} (79%)
rename content/{media => photos/2017-02-13-streets-of-el-born}/streets-of-el-born.jpg (100%)
rename content/photos/{2017-02-14-new-passion-facade.md => 2017-02-14-new-passion-facade/index.md} (90%)
rename content/{media => photos/2017-02-14-new-passion-facade}/new-passion-facade.jpg (100%)
rename content/{media => photos/2017-02-16-coolhaven-rotterdam}/coolhaven-rotterdam.jpg (100%)
rename content/photos/{2017-02-16-coolhaven-rotterdam.md => 2017-02-16-coolhaven-rotterdam/index.md} (64%)
rename content/{media => photos/2017-02-17-behind-the-art}/behind-the-art.jpg (100%)
rename content/photos/{2017-02-17-behind-the-art.md => 2017-02-17-behind-the-art/index.md} (66%)
rename content/photos/{ => 2017-02-19-rotterdam-coats}/2017-02-19-rotterdam-coats.jpg (100%)
rename content/photos/{2017-02-19-rotterdam-coats.md => 2017-02-19-rotterdam-coats/index.md} (85%)
rename content/photos/{ => 2017-02-21-david-chipperfield-staircase}/2017-02-21-david-chipperfield-staircase.jpg (100%)
rename content/photos/{2017-02-21-david-chipperfield-staircase.md => 2017-02-21-david-chipperfield-staircase/index.md} (82%)
rename content/photos/{ => 2017-02-26-eu-gotham-city}/2017-02-26-eu-gotham-city.jpg (100%)
rename content/photos/{2017-02-26-eu-gotham-city.md => 2017-02-26-eu-gotham-city/index.md} (85%)
rename content/photos/{ => 2017-02-27-amsterdam-cliche}/2017-02-27-amsterdam-cliche.jpg (100%)
rename content/photos/{2017-02-27-amsterdam-cliche.md => 2017-02-27-amsterdam-cliche/index.md} (85%)
rename content/photos/{ => 2017-02-27-its-dark-and-i-dont-exist}/2017-02-27-its-dark-and-i-dont-exist.jpg (100%)
rename content/photos/{2017-02-27-its-dark-and-i-dont-exist.md => 2017-02-27-its-dark-and-i-dont-exist/index.md} (73%)
rename content/photos/{ => 2017-02-28-stedelijk-museum}/2017-02-28-stedelijk-museum.jpg (100%)
rename content/photos/{2017-02-28-stedelijk-museum.md => 2017-02-28-stedelijk-museum/index.md} (78%)
rename content/photos/{ => 2017-02-28-temple-guardian-rijksmuseum}/2017-02-28-temple-guardian-rijksmuseum.jpg (100%)
rename content/photos/{2017-02-28-temple-guardian-rijksmuseum.md => 2017-02-28-temple-guardian-rijksmuseum/index.md} (84%)
rename content/photos/{ => 2017-02-28-watching-the-night-watch}/2017-02-28-watching-the-night-watch.jpg (100%)
rename content/photos/{2017-02-28-watching-the-night-watch.md => 2017-02-28-watching-the-night-watch/index.md} (82%)
rename content/photos/{ => 2017-04-16-hamburgs-elbphilharmonie}/2017-04-16-hamburgs-elbphilharmonie.jpg (100%)
rename content/photos/{2017-04-16-hamburgs-elbphilharmonie.md => 2017-04-16-hamburgs-elbphilharmonie/index.md} (80%)
rename content/photos/{ => 2017-04-29-palace-scaffolding}/2017-04-29-palace-scaffolding.jpg (100%)
rename content/photos/{2017-04-29-palace-scaffolding.md => 2017-04-29-palace-scaffolding/index.md} (78%)
rename content/photos/{ => 2017-07-05-kapaleeshwarar-temple-chennai}/2017-07-05-kapaleeshwarar-temple-chennai.jpg (100%)
rename content/photos/{2017-07-05-kapaleeshwarar-temple-chennai.md => 2017-07-05-kapaleeshwarar-temple-chennai/index.md} (79%)
rename content/photos/{ => 2017-07-08-kochis-streetart-game-is-strong}/2017-07-08-kochis-streetart-game-is-strong.jpg (100%)
rename content/photos/{2017-07-08-kochis-streetart-game-is-strong.md => 2017-07-08-kochis-streetart-game-is-strong/index.md} (81%)
rename content/photos/{ => 2017-07-09-orphaned-elephant-with-friend}/2017-07-09-orphaned-elephant-with-friend.jpg (100%)
rename content/photos/{2017-07-09-orphaned-elephant-with-friend.md => 2017-07-09-orphaned-elephant-with-friend/index.md} (78%)
rename content/photos/{ => 2017-07-10-kochis-dhobhi-ghat}/2017-07-10-kochis-dhobhi-ghat.jpg (100%)
rename content/photos/{2017-07-10-kochis-dhobhi-ghat.md => 2017-07-10-kochis-dhobhi-ghat/index.md} (85%)
rename content/photos/{ => 2017-07-13-mumbai-hand-painted-typography}/2017-07-13-mumbai-hand-painted-typography.jpg (100%)
rename content/photos/{2017-07-13-mumbai-hand-painted-typography.md => 2017-07-13-mumbai-hand-painted-typography/index.md} (83%)
rename content/photos/{ => 2017-07-13-obligatory-gate-of-india-photo}/2017-07-13-obligatory-gate-of-india-photo.jpg (100%)
rename content/photos/{2017-07-13-obligatory-gate-of-india-photo.md => 2017-07-13-obligatory-gate-of-india-photo/index.md} (79%)
rename content/photos/{ => 2017-07-14-chhatrapati-shivaji-maharaj-vastu-sangrahalaya}/2017-07-14-chhatrapati-shivaji-maharaj-vastu-sangrahalaya.jpg (100%)
rename content/photos/{2017-07-14-chhatrapati-shivaji-maharaj-vastu-sangrahalaya.md => 2017-07-14-chhatrapati-shivaji-maharaj-vastu-sangrahalaya/index.md} (82%)
rename content/photos/{ => 2017-11-10-acropolis-the-erechtheum}/2017-11-10-acropolis-the-erechtheum.jpg (100%)
rename content/photos/{2017-11-10-acropolis-the-erechtheum.md => 2017-11-10-acropolis-the-erechtheum/index.md} (79%)
rename content/photos/{ => 2017-11-10-acropolis-the-parthenon}/2017-11-10-acropolis-the-parthenon.jpg (100%)
rename content/photos/{2017-11-10-acropolis-the-parthenon.md => 2017-11-10-acropolis-the-parthenon/index.md} (79%)
rename content/photos/{ => 2017-11-10-acropolis-the-propylaea}/2017-11-10-acropolis-the-propylaea.jpg (100%)
rename content/photos/{2017-11-10-acropolis-the-propylaea.md => 2017-11-10-acropolis-the-propylaea/index.md} (80%)
rename content/photos/{ => 2017-12-15-el-born-centre-de-cultura-i-memoria}/2017-12-15-el-born-centre-de-cultura-i-memoria.jpg (100%)
rename content/photos/{2017-12-15-el-born-centre-de-cultura-i-memoria.md => 2017-12-15-el-born-centre-de-cultura-i-memoria/index.md} (75%)
rename content/photos/{ => 2017-12-16-sagrada-familia}/2017-12-16-sagrada-familia.jpg (100%)
rename content/photos/{2017-12-16-sagrada-familia.md => 2017-12-16-sagrada-familia/index.md} (74%)
rename content/photos/{ => 2017-12-27-sao-paulo-traffic}/2017-12-27-sao-paulo-traffic.jpg (100%)
rename content/photos/{2017-12-27-sao-paulo-traffic.md => 2017-12-27-sao-paulo-traffic/index.md} (73%)
rename content/photos/{ => 2018-01-04-passagem-literaria-da-consolacao}/2018-01-04-passagem-literaria-da-consolacao.jpg (100%)
rename content/photos/{2018-01-04-passagem-literaria-da-consolacao.md => 2018-01-04-passagem-literaria-da-consolacao/index.md} (72%)
rename content/photos/{ => 2018-01-05-samba-school}/2018-01-05-samba-school.jpg (100%)
rename content/photos/{2018-01-05-samba-school.md => 2018-01-05-samba-school/index.md} (74%)
rename content/photos/{ => 2018-01-10-smoking-death}/2018-01-10-smoking-death.jpg (100%)
rename content/photos/{2018-01-10-smoking-death.md => 2018-01-10-smoking-death/index.md} (78%)
rename content/photos/{ => 2018-01-13-teatro-jaragua}/2018-01-13-teatro-jaragua.jpg (100%)
rename content/photos/{2018-01-13-teatro-jaragua.md => 2018-01-13-teatro-jaragua/index.md} (78%)
rename content/photos/{ => 2018-01-15-a-revolucao-nao-sera-televisionada}/2018-01-15-a-revolucao-nao-sera-televisionada.jpg (100%)
rename content/photos/{2018-01-15-a-revolucao-nao-sera-televisionada.md => 2018-01-15-a-revolucao-nao-sera-televisionada/index.md} (68%)
rename content/photos/{ => 2018-01-15-fusca-e-pichacao}/2018-01-15-fusca-e-pichacao.jpg (100%)
rename content/photos/{2018-01-15-fusca-e-pichacao.md => 2018-01-15-fusca-e-pichacao/index.md} (67%)
rename content/photos/{ => 2018-01-17-boa-viagem}/2018-01-17-boa-viagem.jpg (100%)
rename content/photos/{2018-01-17-boa-viagem.md => 2018-01-17-boa-viagem/index.md} (71%)
rename content/photos/{ => 2018-01-17-instituto-ricardo-brennand}/2018-01-17-instituto-ricardo-brennand.jpg (100%)
rename content/photos/{2018-01-17-instituto-ricardo-brennand.md => 2018-01-17-instituto-ricardo-brennand/index.md} (76%)
rename content/photos/{ => 2018-01-24-avenida-paulista-i}/2018-01-24-avenida-paulista-i.jpg (100%)
rename content/photos/{2018-01-24-avenida-paulista-i.md => 2018-01-24-avenida-paulista-i/index.md} (70%)
rename content/photos/{ => 2018-01-25-avenida-paulista-ii}/2018-01-25-avenida-paulista-ii.jpg (100%)
rename content/photos/{2018-01-25-avenida-paulista-ii.md => 2018-01-25-avenida-paulista-ii/index.md} (80%)
rename content/photos/{ => 2018-01-26-auditorio-ibirapuera}/2018-01-26-auditorio-ibirapuera.jpg (100%)
rename content/photos/{2018-01-26-auditorio-ibirapuera.md => 2018-01-26-auditorio-ibirapuera/index.md} (84%)
rename content/photos/{ => 2018-01-26-oca-do-ibirapuera-i}/2018-01-26-oca-do-ibirapuera-i.jpg (100%)
rename content/photos/{2018-01-26-oca-do-ibirapuera-i.md => 2018-01-26-oca-do-ibirapuera-i/index.md} (87%)
rename content/photos/{ => 2018-01-26-oca-do-ibirapuera-ii}/2018-01-26-oca-do-ibirapuera-ii.jpg (100%)
rename content/photos/{2018-01-26-oca-do-ibirapuera-ii.md => 2018-01-26-oca-do-ibirapuera-ii/index.md} (87%)
rename content/photos/{ => 2018-01-26-pavilhao-das-culturas-brasileiras}/2018-01-26-pavilhao-das-culturas-brasileiras.jpg (100%)
rename content/photos/{2018-01-26-pavilhao-das-culturas-brasileiras.md => 2018-01-26-pavilhao-das-culturas-brasileiras/index.md} (83%)
rename content/photos/{ => 2018-02-24-muzeul-national-de-arta-al-romaniei}/2018-02-24-muzeul-national-de-arta-al-romaniei.jpg (100%)
rename content/photos/{2018-02-24-muzeul-national-de-arta-al-romaniei.md => 2018-02-24-muzeul-national-de-arta-al-romaniei/index.md} (77%)
rename content/photos/{ => 2018-02-25-muzeul-national-de-istorie-a-romaniei}/2018-02-25-muzeul-national-de-istorie-a-romaniei.jpg (100%)
rename content/photos/{2018-02-25-muzeul-national-de-istorie-a-romaniei.md => 2018-02-25-muzeul-national-de-istorie-a-romaniei/index.md} (78%)
rename content/photos/{ => 2018-05-01-may-day-kreuzberg}/2018-05-01-may-day-kreuzberg.jpg (100%)
rename content/photos/{2018-05-01-may-day-kreuzberg.md => 2018-05-01-may-day-kreuzberg/index.md} (80%)
rename content/photos/{ => 2018-06-01-roger-waters-us-them}/2018-06-01-roger-waters-us-them.jpg (100%)
rename content/photos/{2018-06-01-roger-waters-us-them.md => 2018-06-01-roger-waters-us-them/index.md} (71%)
rename content/photos/{ => 2018-07-02-nine-inch-nails-zitadelle}/2018-07-02-nine-inch-nails-zitadelle.jpg (100%)
rename content/photos/{2018-07-02-nine-inch-nails-zitadelle.md => 2018-07-02-nine-inch-nails-zitadelle/index.md} (72%)
rename content/photos/{ => 2018-09-01-stasi-museum}/2018-09-01-stasi-museum.jpg (100%)
rename content/photos/{2018-09-01-stasi-museum.md => 2018-09-01-stasi-museum/index.md} (74%)
rename content/photos/{ => 2018-12-26-elevador-de-santa-justa}/2018-12-26-elevador-de-santa-justa.jpg (100%)
rename content/photos/{2018-12-26-elevador-de-santa-justa.md => 2018-12-26-elevador-de-santa-justa/index.md} (63%)
rename content/photos/{ => 2018-12-27-palacio-nacional-da-pena}/2018-12-27-palacio-nacional-da-pena.jpg (100%)
rename content/photos/{2018-12-27-palacio-nacional-da-pena.md => 2018-12-27-palacio-nacional-da-pena/index.md} (73%)
rename content/photos/{ => 2018-12-27-rua-da-prata}/2018-12-27-rua-da-prata.jpg (100%)
rename content/photos/{2018-12-27-rua-da-prata.md => 2018-12-27-rua-da-prata/index.md} (63%)
rename content/photos/{ => 2018-12-29-gare-do-oriente}/2018-12-29-gare-do-oriente.jpg (100%)
rename content/photos/{2018-12-29-gare-do-oriente.md => 2018-12-29-gare-do-oriente/index.md} (79%)
rename content/photos/{ => 2019-01-27-all-work-and-no-play}/2019-01-27-all-work-and-no-play.jpg (100%)
rename content/photos/{2019-01-27-all-work-and-no-play.md => 2019-01-27-all-work-and-no-play/index.md} (86%)
rename content/photos/{ => 2019-02-25-edificio-italia}/2019-02-25-edificio-italia.jpg (100%)
rename content/photos/{2019-02-25-edificio-italia.md => 2019-02-25-edificio-italia/index.md} (76%)
rename content/photos/{ => 2019-03-06-brazilian-museum-of-sculpture-and-ecology}/2019-03-06-brazilian-museum-of-sculpture-and-ecology.jpg (100%)
rename content/photos/{2019-03-06-brazilian-museum-of-sculpture-and-ecology.md => 2019-03-06-brazilian-museum-of-sculpture-and-ecology/index.md} (73%)
rename content/photos/{ => 2019-03-13-paraty-mirim}/2019-03-13-paraty-mirim.jpg (100%)
rename content/photos/{2019-03-13-paraty-mirim.md => 2019-03-13-paraty-mirim/index.md} (76%)
rename content/photos/{ => 2019-03-18-catedral-da-se-de-sao-paulo}/2019-03-18-catedral-da-se-de-sao-paulo.jpg (100%)
rename content/photos/{2019-03-18-catedral-da-se-de-sao-paulo.md => 2019-03-18-catedral-da-se-de-sao-paulo/index.md} (81%)
rename content/photos/{ => 2019-08-18-german-chancellery-ii}/2019-08-18-german-chancellery-ii.jpg (100%)
rename content/photos/{2019-08-18-german-chancellery-ii.md => 2019-08-18-german-chancellery-ii/index.md} (73%)
rename content/photos/{ => 2019-09-28-vatican-museums}/2019-09-28-vatican-museums.jpg (100%)
rename content/photos/{2019-09-28-vatican-museums.md => 2019-09-28-vatican-museums/index.md} (80%)
rename content/photos/{ => 2019-09-29-arco-di-costantino}/2019-09-29-arco-di-costantino.jpg (100%)
rename content/photos/{2019-09-29-arco-di-costantino.md => 2019-09-29-arco-di-costantino/index.md} (76%)
rename content/photos/{ => 2019-09-29-foro-di-cesare}/2019-09-29-foro-di-cesare.jpg (100%)
rename content/photos/{2019-09-29-foro-di-cesare.md => 2019-09-29-foro-di-cesare/index.md} (85%)
rename content/photos/{ => 2019-11-02-orszaghaz-i}/2019-11-02-orszaghaz-i.jpg (100%)
rename content/photos/{2019-11-02-orszaghaz-i.md => 2019-11-02-orszaghaz-i/index.md} (80%)
rename content/photos/{ => 2019-11-03-orszaghaz-ii}/2019-11-03-orszaghaz-ii.jpg (100%)
rename content/photos/{2019-11-03-orszaghaz-ii.md => 2019-11-03-orszaghaz-ii/index.md} (77%)
rename content/photos/{ => 2019-11-03-orszaghaz-iii}/2019-11-03-orszaghaz-iii.jpg (100%)
rename content/photos/{2019-11-03-orszaghaz-iii.md => 2019-11-03-orszaghaz-iii/index.md} (76%)
rename content/photos/{ => 2019-11-16-helmut-newton-foundation}/2019-11-16-helmut-newton-foundation.jpg (100%)
rename content/photos/{2019-11-16-helmut-newton-foundation.md => 2019-11-16-helmut-newton-foundation/index.md} (85%)
rename content/photos/{ => 2020-01-05-raw-gelande}/2020-01-05-raw-gelande.jpg (100%)
rename content/photos/{2020-01-05-raw-gelande.md => 2020-01-05-raw-gelande/index.md} (85%)
rename content/photos/{ => 2020-01-17-balloon-dog}/2020-01-17-balloon-dog.jpg (100%)
rename content/photos/{2020-01-17-balloon-dog.md => 2020-01-17-balloon-dog/index.md} (85%)
rename content/photos/{ => 2020-01-17-bremen-cathedral}/2020-01-17-bremen-cathedral.jpg (100%)
rename content/photos/{2020-01-17-bremen-cathedral.md => 2020-01-17-bremen-cathedral/index.md} (82%)
rename content/photos/{ => 2020-06-14-x-marks-the-spot}/2020-06-14-x-marks-the-spot.jpg (100%)
rename content/photos/{2020-06-14-x-marks-the-spot.md => 2020-06-14-x-marks-the-spot/index.md} (77%)
rename content/photos/{ => 2020-06-28-lines-of-nature}/2020-06-28-lines-of-nature.jpg (100%)
rename content/photos/{2020-06-28-lines-of-nature.md => 2020-06-28-lines-of-nature/index.md} (61%)
rename content/photos/{ => 2020-08-13-2020}/2020-08-13-2020.jpg (100%)
rename content/photos/{2020-08-13-2020.md => 2020-08-13-2020/index.md} (65%)
rename content/photos/{ => 2020-08-16-castle-gardens}/2020-08-16-castle-gardens.jpg (100%)
rename content/photos/{2020-08-16-castle-gardens.md => 2020-08-16-castle-gardens/index.md} (79%)
rename content/photos/{ => 2020-09-12-friedrichshain}/2020-09-12-friedrichshain.jpg (100%)
rename content/photos/{2020-09-12-friedrichshain.md => 2020-09-12-friedrichshain/index.md} (61%)
rename content/photos/{ => 2020-10-11-charite}/2020-10-11-charite.jpg (100%)
rename content/photos/{2020-10-11-charite.md => 2020-10-11-charite/index.md} (77%)
rename content/photos/{ => 2020-10-25-letters-change}/2020-10-25-letters-change.jpg (100%)
rename content/photos/{2020-10-25-letters-change.md => 2020-10-25-letters-change/index.md} (81%)
rename content/photos/{ => 2020-11-29-wall-memorial}/2020-11-29-wall-memorial.jpg (100%)
rename content/photos/{2020-11-29-wall-memorial.md => 2020-11-29-wall-memorial/index.md} (73%)
rename content/photos/{ => 2020-12-22-tier-pandemic}/2020-12-22-tier-pandemic.jpg (100%)
rename content/photos/{2020-12-22-tier-pandemic.md => 2020-12-22-tier-pandemic/index.md} (73%)
rename content/photos/{ => 2021-02-21-reflections}/2021-02-21-reflections.jpg (100%)
rename content/photos/{2021-02-21-reflections.md => 2021-02-21-reflections/index.md} (79%)
rename content/photos/{ => 2021-07-07-museum-neuruppin-staircase}/2021-07-07-museum-neuruppin-staircase.jpg (100%)
rename content/photos/{2021-07-07-museum-neuruppin-staircase.md => 2021-07-07-museum-neuruppin-staircase/index.md} (76%)
rename content/photos/{ => 2021-08-05-maidan-nezalezhnosti}/2021-08-05-maidan-nezalezhnosti.jpg (100%)
rename content/photos/{2021-08-05-maidan-nezalezhnosti.md => 2021-08-05-maidan-nezalezhnosti/index.md} (82%)
rename content/photos/{ => 2021-08-06-hram-arhistratiga-mihayila-ta-ukrayinskih-novomuchenikiv}/2021-08-06-hram-arhistratiga-mihayila-ta-ukrayinskih-novomuchenikiv.jpg (100%)
rename content/photos/{2021-08-06-hram-arhistratiga-mihayila-ta-ukrayinskih-novomuchenikiv.md => 2021-08-06-hram-arhistratiga-mihayila-ta-ukrayinskih-novomuchenikiv/index.md} (84%)
rename content/photos/{ => 2021-08-08-kyiv-funicular}/2021-08-08-kyiv-funicular.jpg (100%)
rename content/photos/{2021-08-08-kyiv-funicular.md => 2021-08-08-kyiv-funicular/index.md} (80%)
rename content/photos/{ => 2021-08-08-kyiv-underground}/2021-08-08-kyiv-underground.jpg (100%)
rename content/photos/{2021-08-08-kyiv-underground.md => 2021-08-08-kyiv-underground/index.md} (60%)
rename content/photos/{ => 2021-08-09-kiyiv-pasazhirskij}/2021-08-09-kiyiv-pasazhirskij.jpg (100%)
rename content/photos/{2021-08-09-kiyiv-pasazhirskij.md => 2021-08-09-kiyiv-pasazhirskij/index.md} (65%)
rename content/photos/{ => 2021-08-17-horticya}/2021-08-17-horticya.jpg (100%)
rename content/photos/{2021-08-17-horticya.md => 2021-08-17-horticya/index.md} (90%)
rename content/photos/{ => 2021-08-17-zaporizhzhya}/2021-08-17-zaporizhzhya.jpg (100%)
rename content/photos/{2021-08-17-zaporizhzhya.md => 2021-08-17-zaporizhzhya/index.md} (81%)
rename content/photos/{ => 2021-09-26-arc-de-triomphe-wrapped}/2021-09-26-arc-de-triomphe-wrapped.jpg (100%)
rename content/photos/{2021-09-26-arc-de-triomphe-wrapped.md => 2021-09-26-arc-de-triomphe-wrapped/index.md} (79%)
rename content/photos/{ => 2021-09-26-centre-pompidou-pigeons}/2021-09-26-centre-pompidou-pigeons.jpg (100%)
rename content/photos/{2021-09-26-centre-pompidou-pigeons.md => 2021-09-26-centre-pompidou-pigeons/index.md} (76%)
rename content/photos/{ => 2021-09-26-louvre-i}/2021-09-26-louvre-i.jpg (100%)
rename content/photos/{2021-09-26-louvre-i.md => 2021-09-26-louvre-i/index.md} (62%)
rename content/photos/{ => 2021-09-26-tour-eiffel}/2021-09-26-tour-eiffel.jpg (100%)
rename content/photos/{2021-09-26-tour-eiffel.md => 2021-09-26-tour-eiffel/index.md} (81%)
rename content/photos/{ => 2021-09-29-louvre-ii}/2021-09-29-louvre-ii.jpg (100%)
rename content/photos/{2021-09-29-louvre-ii.md => 2021-09-29-louvre-ii/index.md} (62%)
rename content/photos/{ => 2021-09-30-tgv-euroduplex}/2021-09-30-tgv-euroduplex.jpg (100%)
rename content/photos/{2021-09-30-tgv-euroduplex.md => 2021-09-30-tgv-euroduplex/index.md} (85%)
rename content/photos/{ => 2021-10-04-santa-maria-la-real-de-la-almudena}/2021-10-04-santa-maria-la-real-de-la-almudena.jpg (100%)
rename content/photos/{2021-10-04-santa-maria-la-real-de-la-almudena.md => 2021-10-04-santa-maria-la-real-de-la-almudena/index.md} (77%)
rename content/photos/{ => 2021-10-09-ponte-da-arrabida}/2021-10-09-ponte-da-arrabida.jpg (100%)
rename content/photos/{2021-10-09-ponte-da-arrabida.md => 2021-10-09-ponte-da-arrabida/index.md} (79%)
rename content/photos/{ => 2021-10-11-garagem}/2021-10-11-garagem.jpg (100%)
rename content/photos/{2021-10-11-garagem.md => 2021-10-11-garagem/index.md} (63%)
rename content/photos/{ => 2021-10-12-ponte-de-dom-luis-i}/2021-10-12-ponte-de-dom-luis-i.jpg (100%)
rename content/photos/{2021-10-12-ponte-de-dom-luis-i.md => 2021-10-12-ponte-de-dom-luis-i/index.md} (80%)
rename content/photos/{ => 2021-10-14-lince-iberico}/2021-10-14-lince-iberico.jpg (100%)
rename content/photos/{2021-10-14-lince-iberico.md => 2021-10-14-lince-iberico/index.md} (82%)
rename content/photos/{ => 2021-10-16-gare-do-oriente-ii}/2021-10-16-gare-do-oriente-ii.jpg (100%)
rename content/photos/{2021-10-16-gare-do-oriente-ii.md => 2021-10-16-gare-do-oriente-ii/index.md} (91%)
rename content/photos/{ => 2021-10-17-maat}/2021-10-17-maat.jpg (100%)
rename content/photos/{2021-10-17-maat.md => 2021-10-17-maat/index.md} (84%)
rename content/photos/{ => 2021-10-17-padrao-dos-descobrimentos}/2021-10-17-padrao-dos-descobrimentos.jpg (100%)
rename content/photos/{2021-10-17-padrao-dos-descobrimentos.md => 2021-10-17-padrao-dos-descobrimentos/index.md} (81%)
rename content/photos/{ => 2021-10-17-ponte-25-de-abril}/2021-10-17-ponte-25-de-abril.jpg (100%)
rename content/photos/{2021-10-17-ponte-25-de-abril.md => 2021-10-17-ponte-25-de-abril/index.md} (83%)
rename content/photos/{ => 2021-10-17-shiny-colonialism}/2021-10-17-shiny-colonialism.jpg (100%)
rename content/photos/{2021-10-17-shiny-colonialism.md => 2021-10-17-shiny-colonialism/index.md} (85%)
rename content/photos/{ => 2021-10-23-santuario-de-cristo-rei}/2021-10-23-santuario-de-cristo-rei.jpg (100%)
rename content/photos/{2021-10-23-santuario-de-cristo-rei.md => 2021-10-23-santuario-de-cristo-rei/index.md} (80%)
rename content/photos/{ => 2021-10-29-ponte-vasco-da-gama}/2021-10-29-ponte-vasco-da-gama.jpg (100%)
rename content/photos/{2021-10-29-ponte-vasco-da-gama.md => 2021-10-29-ponte-vasco-da-gama/index.md} (84%)
rename content/photos/{ => 2021-10-31-sagrada-familia-ii}/2021-10-31-sagrada-familia-ii.jpg (100%)
rename content/photos/{2021-10-31-sagrada-familia-ii.md => 2021-10-31-sagrada-familia-ii/index.md} (88%)
rename content/photos/{ => 2021-11-06-mae-d-agua}/2021-11-06-mae-d-agua.jpg (100%)
rename content/photos/{2021-11-06-mae-d-agua.md => 2021-11-06-mae-d-agua/index.md} (88%)
rename content/photos/{ => 2021-11-21-assembleia-da-republica}/2021-11-21-assembleia-da-republica.jpg (100%)
rename content/photos/{2021-11-21-assembleia-da-republica.md => 2021-11-21-assembleia-da-republica/index.md} (85%)
rename content/photos/{ => 2021-11-25-praca-do-comercio}/2021-11-25-praca-do-comercio.jpg (100%)
rename content/photos/{2021-11-25-praca-do-comercio.md => 2021-11-25-praca-do-comercio/index.md} (87%)
rename content/photos/{ => 2021-11-26-forever-bicycles}/2021-11-26-forever-bicycles.jpg (100%)
rename content/photos/{2021-11-26-forever-bicycles.md => 2021-11-26-forever-bicycles/index.md} (89%)
rename content/photos/{ => 2021-11-26-law-of-the-journey}/2021-11-26-law-of-the-journey.jpg (100%)
rename content/photos/{2021-11-26-law-of-the-journey.md => 2021-11-26-law-of-the-journey/index.md} (92%)
delete mode 100644 gatsby-browser.tsx
delete mode 100644 gatsby-config.ts
delete mode 100644 gatsby-node.ts
delete mode 100644 gatsby-ssr.tsx
delete mode 100644 gatsby/algolia.ts
delete mode 100644 gatsby/createExif.ts
delete mode 100644 gatsby/createMarkdownFields.ts
delete mode 100644 gatsby/createPages.ts
delete mode 100644 gatsby/feeds.ts
delete mode 100644 gatsby/sources.ts
rename {static => public}/robots.txt (77%)
rename {static => public}/sw.js (100%)
create mode 100644 scripts/create-icons/Props.d.ts
create mode 100644 scripts/create-icons/index.test.ts
create mode 100644 scripts/create-icons/index.ts
create mode 100644 scripts/create-icons/svg.ts
create mode 100755 scripts/create-symlinks.sh
create mode 100644 scripts/move-downloads.test.ts
create mode 100644 scripts/move-downloads.ts
delete mode 100644 scripts/new.ts
create mode 100644 scripts/new/createArticlePost.ts
create mode 100644 scripts/new/createPhotoPost.ts
create mode 100644 scripts/new/index.test.ts
create mode 100644 scripts/new/index.ts
rename scripts/{ => new}/new-article.md (70%)
rename scripts/{ => new}/new-photo.md (53%)
create mode 100644 scripts/redirect-from.test.ts
create mode 100644 scripts/redirect-from.ts
delete mode 100644 src/@types/Image.d.ts
delete mode 100644 src/@types/Post.d.ts
delete mode 100644 src/@types/css.d.ts
rename src/@types/{node_modules.d.ts => dmsdec/index.d.ts} (63%)
create mode 100644 src/@types/node-iptc/index.d.ts
create mode 100644 src/components/BackButton.astro
create mode 100644 src/components/Changelog/index.astro
rename src/components/{atoms/Changelog.module.css => Changelog/index.module.css} (81%)
create mode 100644 src/components/Copy.astro
rename src/components/{atoms => }/Divider.module.css (85%)
create mode 100644 src/components/Donation/Coin.astro
create mode 100644 src/components/Donation/Web3.tsx
rename src/components/{molecules => Donation}/Web3Donation/Alert.module.css (100%)
rename src/components/{molecules => Donation}/Web3Donation/Alert.test.tsx (88%)
rename src/components/{molecules => Donation}/Web3Donation/Alert.tsx (83%)
rename src/components/{molecules => Donation}/Web3Donation/Conversion.module.css (72%)
rename src/components/{molecules => Donation}/Web3Donation/Conversion.test.tsx (66%)
rename src/components/{molecules => Donation}/Web3Donation/Conversion.tsx (61%)
rename src/components/{molecules => Donation}/Web3Donation/InputGroup.module.css (95%)
create mode 100644 src/components/Donation/Web3Donation/InputGroup.test.tsx
rename src/components/{molecules => Donation}/Web3Donation/InputGroup.tsx (59%)
rename src/components/{molecules => Donation}/Web3Donation/index.module.css (77%)
create mode 100644 src/components/Donation/Web3Donation/index.test.tsx
rename src/components/{molecules => Donation}/Web3Donation/index.tsx (70%)
create mode 100644 src/components/Exif/ExifData.astro
create mode 100644 src/components/Exif/ExifMap.test.tsx
create mode 100644 src/components/Exif/ExifMap.tsx
create mode 100644 src/components/Exif/index.astro
rename src/components/{atoms/Exif.module.css => Exif/index.module.css} (89%)
create mode 100644 src/components/Footer/Networks.astro
rename src/components/{molecules => Footer}/Networks.module.css (100%)
create mode 100644 src/components/Footer/Vcard.astro
rename src/components/{molecules => Footer}/Vcard.module.css (66%)
create mode 100644 src/components/Footer/index.astro
rename src/components/{organisms/Footer.module.css => Footer/index.module.css} (100%)
create mode 100644 src/components/Hamburger.astro
create mode 100644 src/components/Header/index.astro
rename src/components/{organisms/Header.module.css => Header/index.module.css} (96%)
rename src/components/{atoms/Input.module.css => Input/index.module.css} (93%)
create mode 100644 src/components/Input/index.test.tsx
rename src/components/{atoms/Input.tsx => Input/index.tsx} (64%)
delete mode 100644 src/components/Layout.test.tsx
delete mode 100644 src/components/Layout.tsx
create mode 100644 src/components/Menu/index.astro
rename src/components/{molecules/Menu.module.css => Menu/index.module.css} (93%)
create mode 100644 src/components/More.astro
create mode 100644 src/components/Pagination/PageNumber.astro
create mode 100644 src/components/Pagination/PrevNext.astro
create mode 100644 src/components/Pagination/index.astro
rename src/components/{molecules/Pagination.module.css => Pagination/index.module.css} (97%)
create mode 100644 src/components/PhotoTeaser.astro
create mode 100644 src/components/Picture/index.astro
rename src/components/{atoms/Image.module.css => Picture/index.module.css} (77%)
create mode 100644 src/components/PostTeaser/index.astro
rename src/components/{molecules/PostTeaser.module.css => PostTeaser/index.module.css} (78%)
create mode 100644 src/components/RelatedPosts/index.astro
rename src/components/{molecules/RelatedPosts.module.css => RelatedPosts/index.module.css} (84%)
rename src/components/{molecules/Search/SearchResultsEmpty.module.css => Search/Results/Empty.module.css} (100%)
rename src/components/{molecules/Search/SearchResultsEmpty.tsx => Search/Results/Empty.tsx} (50%)
rename src/components/{molecules/Search/SearchResults.module.css => Search/Results/index.module.css} (70%)
create mode 100644 src/components/Search/Results/index.tsx
rename src/components/{molecules/Search/SearchInput.module.css => Search/Search.module.css} (66%)
create mode 100644 src/components/Search/Search.test.tsx
create mode 100644 src/components/Search/Search.tsx
create mode 100644 src/components/Search/index.astro
create mode 100644 src/components/Tag.astro
create mode 100644 src/components/ThemeSwitch/index.astro
rename src/components/{molecules/ThemeSwitch.module.css => ThemeSwitch/index.module.css} (91%)
create mode 100644 src/components/ThemeSwitch/theme.cjs
create mode 100644 src/components/ThemeSwitch/theme.test.ts
create mode 100644 src/components/Time.astro
create mode 100644 src/components/Toc.astro
delete mode 100644 src/components/atoms/Changelog.test.tsx
delete mode 100644 src/components/atoms/Changelog.tsx
delete mode 100644 src/components/atoms/Copy.module.css
delete mode 100644 src/components/atoms/Copy.test.tsx
delete mode 100644 src/components/atoms/Copy.tsx
delete mode 100644 src/components/atoms/Exif.test.tsx
delete mode 100644 src/components/atoms/Exif.tsx
delete mode 100644 src/components/atoms/ExifMap.tsx
delete mode 100644 src/components/atoms/Hamburger.module.css
delete mode 100644 src/components/atoms/Hamburger.tsx
delete mode 100644 src/components/atoms/HeadMeta/SchemaOrg.tsx
delete mode 100644 src/components/atoms/HeadMeta/index.tsx
delete mode 100644 src/components/atoms/Icon.module.css
delete mode 100644 src/components/atoms/Icon.test.tsx
delete mode 100644 src/components/atoms/Icon.tsx
delete mode 100644 src/components/atoms/Image.tsx
delete mode 100644 src/components/atoms/Input.test.tsx
delete mode 100644 src/components/atoms/Tag.module.css
delete mode 100644 src/components/atoms/Tag.tsx
delete mode 100644 src/components/atoms/Time.tsx
delete mode 100644 src/components/atoms/Transitions.ts
delete mode 100644 src/components/atoms/Typekit.tsx
create mode 100644 src/components/layouts/Archive.astro
create mode 100644 src/components/layouts/Base/Head.astro
create mode 100644 src/components/layouts/Base/SchemaOrg.astro
create mode 100644 src/components/layouts/Base/index.astro
rename src/components/{Layout.module.css => layouts/Base/index.module.css} (75%)
create mode 100644 src/components/layouts/Post/Action.astro
create mode 100644 src/components/layouts/Post/Actions.astro
create mode 100644 src/components/layouts/Post/Actions.module.css
create mode 100644 src/components/layouts/Post/Date.astro
create mode 100644 src/components/layouts/Post/LinkActions.astro
rename src/components/{templates => layouts}/Post/LinkActions.module.css (91%)
create mode 100644 src/components/layouts/Post/Meta.astro
rename src/components/{templates => layouts}/Post/Meta.module.css (100%)
create mode 100644 src/components/layouts/Post/PrevNext.astro
rename src/components/{templates => layouts}/Post/PrevNext.module.css (100%)
create mode 100644 src/components/layouts/Post/Title.astro
rename src/components/{templates => layouts}/Post/Title.module.css (85%)
create mode 100644 src/components/layouts/Post/index.astro
create mode 100644 src/components/layouts/Post/index.module.css
delete mode 100644 src/components/molecules/Menu.tsx
delete mode 100644 src/components/molecules/Networks.test.tsx
delete mode 100644 src/components/molecules/Networks.tsx
delete mode 100644 src/components/molecules/Pagination.tsx
delete mode 100644 src/components/molecules/PostDate.module.css
delete mode 100644 src/components/molecules/PostDate.tsx
delete mode 100644 src/components/molecules/PostTeaser.test.tsx
delete mode 100644 src/components/molecules/PostTeaser.tsx
delete mode 100644 src/components/molecules/RelatedPosts.test.tsx
delete mode 100644 src/components/molecules/RelatedPosts.tsx
delete mode 100644 src/components/molecules/Search/SearchButton.module.css
delete mode 100644 src/components/molecules/Search/SearchButton.tsx
delete mode 100644 src/components/molecules/Search/SearchInput.tsx
delete mode 100644 src/components/molecules/Search/SearchResults.tsx
delete mode 100644 src/components/molecules/Search/index.module.css
delete mode 100644 src/components/molecules/Search/index.tsx
delete mode 100644 src/components/molecules/ThemeSwitch.test.tsx
delete mode 100644 src/components/molecules/ThemeSwitch.tsx
delete mode 100644 src/components/molecules/Vcard.tsx
delete mode 100644 src/components/molecules/Web3Donation/InputGroup.test.tsx
delete mode 100644 src/components/organisms/Footer.test.tsx
delete mode 100644 src/components/organisms/Footer.tsx
delete mode 100644 src/components/organisms/Header.test.tsx
delete mode 100644 src/components/organisms/Header.tsx
delete mode 100644 src/components/templates/Archive.module.css
delete mode 100644 src/components/templates/Archive.test.tsx
delete mode 100644 src/components/templates/Archive.tsx
delete mode 100644 src/components/templates/Page.module.css
delete mode 100644 src/components/templates/Page.tsx
delete mode 100644 src/components/templates/Photos.module.css
delete mode 100644 src/components/templates/Photos.test.tsx
delete mode 100644 src/components/templates/Photos.tsx
delete mode 100644 src/components/templates/Post/Actions.module.css
delete mode 100644 src/components/templates/Post/Actions.tsx
delete mode 100644 src/components/templates/Post/Content.module.css
delete mode 100644 src/components/templates/Post/Content.tsx
delete mode 100644 src/components/templates/Post/Lead.module.css
delete mode 100644 src/components/templates/Post/Lead.tsx
delete mode 100644 src/components/templates/Post/LinkActions.tsx
delete mode 100644 src/components/templates/Post/Meta.tsx
delete mode 100644 src/components/templates/Post/More.module.css
delete mode 100644 src/components/templates/Post/More.tsx
delete mode 100644 src/components/templates/Post/PrevNext.tsx
delete mode 100644 src/components/templates/Post/Title.tsx
delete mode 100644 src/components/templates/Post/Toc.module.css
delete mode 100644 src/components/templates/Post/Toc.tsx
delete mode 100644 src/components/templates/Post/index.module.css
delete mode 100644 src/components/templates/Post/index.test.tsx
delete mode 100644 src/components/templates/Post/index.tsx
create mode 100644 src/env.d.ts
delete mode 100644 src/helpers/umami.ts
delete mode 100644 src/helpers/wrapPageElement.test.tsx
delete mode 100644 src/helpers/wrapPageElement.tsx
delete mode 100644 src/hooks/useDarkMode.ts
delete mode 100644 src/hooks/useSiteMetadata.ts
delete mode 100644 src/images/apple-touch-icon.png
delete mode 100644 src/images/favicon-16x16.png
delete mode 100644 src/images/favicon-32x32.png
delete mode 100644 src/images/favicon-96x96.png
delete mode 100644 src/images/favicon-mask.svg
create mode 100644 src/images/favicon.png
create mode 100644 src/images/favicon.svg
delete mode 100644 src/images/kremalicious1024.png
delete mode 100644 src/images/kremalicious512.png
delete mode 100644 src/images/krlcus-cloud16.png
delete mode 100644 src/images/krlcus-cloud32.png
delete mode 100644 src/images/logo-kremalicious-g-profile.png
delete mode 100644 src/images/metro-tile.png
delete mode 100644 src/images/touch-icon-192x192.png
create mode 100644 src/lib/astro/astro.test.ts
create mode 100644 src/lib/astro/getAllPosts.ts
create mode 100644 src/lib/astro/getAllPostsForSearch.ts
create mode 100644 src/lib/astro/getAllTags.ts
create mode 100644 src/lib/astro/getPostsByTag.ts
create mode 100644 src/lib/astro/index.ts
create mode 100644 src/lib/astro/loadAndFormatCollection.ts
create mode 100644 src/lib/astro/sortPosts.ts
create mode 100644 src/lib/exif/format.ts
create mode 100644 src/lib/exif/index.ts
create mode 100644 src/lib/exif/types.d.ts
create mode 100644 src/lib/feed.test.ts
create mode 100644 src/lib/feed.ts
create mode 100644 src/lib/github.test.ts
create mode 100644 src/lib/github.ts
create mode 100644 src/lib/markdown.ts
rename src/{helpers => lib}/rainbowkit.ts (80%)
create mode 100644 src/lib/remark-lead-paragraph.test.ts
create mode 100644 src/lib/remark-lead-paragraph.ts
create mode 100644 src/lib/remark-toc.test.ts
create mode 100644 src/lib/remark-toc.ts
create mode 100644 src/lib/slugify.test.ts
create mode 100644 src/lib/slugify.ts
create mode 100644 src/lib/umami.test.ts
create mode 100644 src/lib/umami.ts
create mode 100644 src/pages/404.astro
delete mode 100644 src/pages/404.module.css
delete mode 100644 src/pages/404.tsx
create mode 100644 src/pages/[...slug].astro
delete mode 100644 src/pages/__tests__/404.test.tsx
delete mode 100644 src/pages/__tests__/index.test.tsx
delete mode 100644 src/pages/__tests__/tags.test.tsx
create mode 100644 src/pages/api/posts.ts
create mode 100644 src/pages/archive/[page].astro
create mode 100644 src/pages/archive/index.astro
create mode 100644 src/pages/favicon.ico.ts
create mode 100644 src/pages/feed.json.ts
create mode 100644 src/pages/feed.xml.ts
create mode 100644 src/pages/index.astro
delete mode 100644 src/pages/index.module.css
delete mode 100644 src/pages/index.tsx
create mode 100644 src/pages/manifest.json.ts
create mode 100644 src/pages/photos/[page].astro
create mode 100644 src/pages/photos/index.astro
rename src/pages/{styleguide/index.md => styleguide.md} (99%)
delete mode 100644 src/pages/tags.module.css
delete mode 100644 src/pages/tags.tsx
create mode 100644 src/pages/tags/[tag].astro
create mode 100644 src/pages/tags/index.astro
create mode 100644 src/pages/thanks.astro
delete mode 100644 src/pages/thanks.module.css
delete mode 100644 src/pages/thanks.tsx
create mode 100644 src/stores/search.ts
rename src/{global => styles}/_alerts.css (88%)
rename src/{global => styles}/_buttons.css (96%)
rename src/{global => styles}/_code.css (51%)
rename src/{global => styles}/_variables.css (93%)
rename src/{global => styles}/global.css (92%)
rename src/{global => styles}/imports.css (100%)
delete mode 100644 static/apple-touch-icon.png
delete mode 100644 static/favicon.ico
create mode 100644 test/__fixtures__/image-with-metadata.jpg
create mode 100644 test/__fixtures__/new-article.md
create mode 100644 test/__fixtures__/new-photo.md
rename {.jest => test}/__mocks__/@rainbow-me/rainbowkit.js (54%)
create mode 100644 test/__mocks__/wagmi.ts
create mode 100644 test/e2e/404.spec.ts
create mode 100644 test/e2e/404.spec.ts-snapshots/matches-screenshot-1-Mobile-Chrome-darwin.png
create mode 100644 test/e2e/404.spec.ts-snapshots/matches-screenshot-1-Mobile-Chrome-linux.png
create mode 100644 test/e2e/404.spec.ts-snapshots/matches-screenshot-1-Mobile-Safari-darwin.png
create mode 100644 test/e2e/404.spec.ts-snapshots/matches-screenshot-1-Mobile-Safari-linux.png
create mode 100644 test/e2e/404.spec.ts-snapshots/matches-screenshot-1-chromium-darwin.png
create mode 100644 test/e2e/404.spec.ts-snapshots/matches-screenshot-1-chromium-linux.png
create mode 100644 test/e2e/404.spec.ts-snapshots/matches-screenshot-1-firefox-darwin.png
create mode 100644 test/e2e/404.spec.ts-snapshots/matches-screenshot-1-firefox-linux.png
create mode 100644 test/e2e/404.spec.ts-snapshots/matches-screenshot-1-webkit-darwin.png
create mode 100644 test/e2e/404.spec.ts-snapshots/matches-screenshot-1-webkit-linux.png
create mode 100644 test/e2e/header.spec.ts
create mode 100644 test/e2e/header.spec.ts-snapshots/matches-screenshot-1-Mobile-Chrome-darwin.png
create mode 100644 test/e2e/header.spec.ts-snapshots/matches-screenshot-1-Mobile-Chrome-linux.png
create mode 100644 test/e2e/header.spec.ts-snapshots/matches-screenshot-1-Mobile-Safari-darwin.png
create mode 100644 test/e2e/header.spec.ts-snapshots/matches-screenshot-1-Mobile-Safari-linux.png
create mode 100644 test/e2e/header.spec.ts-snapshots/matches-screenshot-1-chromium-darwin.png
create mode 100644 test/e2e/header.spec.ts-snapshots/matches-screenshot-1-chromium-linux.png
create mode 100644 test/e2e/header.spec.ts-snapshots/matches-screenshot-1-firefox-darwin.png
create mode 100644 test/e2e/header.spec.ts-snapshots/matches-screenshot-1-firefox-linux.png
create mode 100644 test/e2e/header.spec.ts-snapshots/matches-screenshot-1-webkit-darwin.png
create mode 100644 test/e2e/header.spec.ts-snapshots/matches-screenshot-1-webkit-linux.png
create mode 100644 test/e2e/index.spec.ts
create mode 100644 test/e2e/photos.spec.ts
create mode 100644 test/e2e/post.spec.ts
create mode 100644 test/e2e/thanks.spec.ts
create mode 100644 test/playwright.config.ts
create mode 100644 test/vitest.config.ts
create mode 100644 test/vitest.setup.ts
delete mode 100644 vendor/polar-0.0.6.vsix
diff --git a/.markdownlint.json b/.config/.markdownlint.json
similarity index 100%
rename from .markdownlint.json
rename to .config/.markdownlint.json
diff --git a/.stylelintrc b/.config/.stylelintrc.json
similarity index 100%
rename from .stylelintrc
rename to .config/.stylelintrc.json
diff --git a/.config/astro.config.ts b/.config/astro.config.ts
new file mode 100644
index 00000000..e0585bdc
--- /dev/null
+++ b/.config/astro.config.ts
@@ -0,0 +1,55 @@
+import { defineConfig } from 'astro/config'
+import remarkLeadParagraph from '../src/lib/remark-lead-paragraph'
+import remarkToc from '../src/lib/remark-toc'
+import react from '@astrojs/react'
+import sitemap from '@astrojs/sitemap'
+import expressiveCode from 'astro-expressive-code'
+import redirects from './redirects.json'
+import config from './blog.config'
+
+// https://astro.build/config
+export default defineConfig({
+ site: config.siteUrl,
+ output: 'static',
+ cacheDir: '.astro',
+ markdown: {
+ remarkPlugins: [remarkLeadParagraph, remarkToc as any],
+ shikiConfig: {
+ // https://github.com/shikijs/shiki/blob/main/docs/themes.md
+ theme: 'nord',
+ langs: [],
+ wrap: true
+ }
+ },
+ server: { host: true },
+ redirects,
+ vite: {
+ resolve: {
+ // for making content -> src/content symlink work
+ // https://www.eliostruyf.com/symlink-content-astro-portability/#fix-the-content-issues
+ preserveSymlinks: true
+ }
+ },
+ integrations: [
+ react(),
+ expressiveCode({
+ theme: 'nord',
+ // https://github.com/expressive-code/expressive-code/blob/ad08cf74095b30055e841d59497990fade634c86/packages/%40expressive-code/core/src/common/core-styles.ts
+ styleOverrides: {
+ borderRadius: 'var(--border-radius)',
+ borderWidth: 'var(--border-width)',
+ uiFontFamily: 'var(--font-family-monospace)',
+ uiFontSize: 'var(--font-size-mini)',
+ codeFontFamily: 'var(--font-family-monospace)',
+ codeFontSize: '0.8rem'
+ }
+ }),
+ sitemap({
+ filter: (page) =>
+ !page.includes('page/') &&
+ !page.includes('tags/') &&
+ !page.includes('archive/') &&
+ !page.includes('404')
+ })
+ ]
+})
diff --git a/.config/aws_redirects.xml b/.config/aws_redirects.xml
new file mode 100644
index 00000000..3f3a00f5
--- /dev/null
+++ b/.config/aws_redirects.xml
@@ -0,0 +1,74 @@
+
+
+
+
+ lab/
+
+
+ lab.kremalicious.com
+
+
+
+
+
+ lab
+
+
+ lab.kremalicious.com
+
+
+
+
+
+ csspaperstack/
+
+
+ lab.kremalicious.com
+
+
+
+
+ csspaperstack
+
+
+ lab.kremalicious.com
+
+
+
+
+ download/
+
+
+ media/
+
+
+
+
+ feed/
+
+
+ feed.xml
+
+
+
+
+ portfolio/
+
+
+ matthiaskretschmann.com
+
+
+
+
+
+ portfolio
+
+
+ matthiaskretschmann.com
+
+
+
+
diff --git a/config.ts b/.config/blog.config.ts
similarity index 81%
rename from config.ts
rename to .config/blog.config.ts
index 00a3f578..22d6f5c2 100644
--- a/config.ts
+++ b/.config/blog.config.ts
@@ -1,15 +1,11 @@
export default {
siteTitle: 'kremalicious',
- siteTitleShort: 'krlc',
siteDescription: 'Blog of designer & developer Matthias Kretschmann',
siteUrl: 'https://kremalicious.com',
- themeColor: '#e7eef4',
- backgroundColor: '#e7eef4',
- pathPrefix: null,
author: {
name: 'Matthias Kretschmann',
email: 'm@kretschmann.io',
- uri: 'https://matthiaskretschmann.com',
+ url: 'https://matthiaskretschmann.com',
twitter: 'https://twitter.com/kremalicious',
mastodon: 'https://mas.to/@krema',
github: 'https://github.com/kremalicious',
@@ -18,7 +14,7 @@ export default {
},
rss: '/feed.xml',
jsonfeed: '/feed.json',
- itemsPerPage: 24,
+ itemsPerPage: 30,
repoContentPath: 'https://github.com/kremalicious/blog/tree/main/content',
menu: [
{
@@ -31,7 +27,7 @@ export default {
},
{
title: 'Goodies',
- link: '/archive/goodies'
+ link: '/tags/goodies'
},
{
title: 'Tags',
diff --git a/.env.sample b/.env.sample
index da2f9b42..ea9f4bcb 100644
--- a/.env.sample
+++ b/.env.sample
@@ -1,7 +1,7 @@
GITHUB_TOKEN=xxx
-GATSBY_MAPBOX_ACCESS_TOKEN=xxx
-GATSBY_TYPEKIT_ID=xxx
-GATSBY_UMAMI_SCRIPT_URL=xxx
-GATSBY_UMAMI_WEBSITE_ID=xxx
-GATSBY_INFURA_ID=xxx
-GATSBY_WALLETCONNECT_ID="xxx"
\ No newline at end of file
+PUBLIC_MAPBOX_ACCESS_TOKEN=xxx
+PUBLIC_TYPEKIT_ID=xxx
+PUBLIC_UMAMI_SCRIPT_URL=xxx
+PUBLIC_UMAMI_WEBSITE_ID=xxx
+PUBLIC_INFURA_ID=xxx
+PUBLIC_WALLETCONNECT_ID="xxx"
\ No newline at end of file
diff --git a/.eslintrc b/.eslintrc.json
similarity index 50%
rename from .eslintrc
rename to .eslintrc.json
index 25a091cb..6316e8c9 100644
--- a/.eslintrc
+++ b/.eslintrc.json
@@ -1,25 +1,23 @@
{
"root": true,
"env": {
+ "es6": true,
"browser": true,
- "node": true,
- "es2020": true,
- "jest": true
+ "node": true
},
"parser": "@typescript-eslint/parser",
"parserOptions": {
- "ecmaVersion": 2020,
+ "ecmaVersion": "latest",
"sourceType": "module",
"ecmaFeatures": { "jsx": true },
"project": "./tsconfig.json",
"tsconfigRootDir": "./"
},
- "plugins": ["@typescript-eslint", "react"],
+ "plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
- "plugin:jsx-a11y/recommended",
- "plugin:react/recommended",
+ "plugin:astro/recommended",
"plugin:prettier/recommended"
],
"rules": {
@@ -29,10 +27,23 @@
"settings": { "react": { "version": "detect" } },
"overrides": [
{
- "files": [
- "**/__tests__/**/*.[jt]s?(x)",
- "**/?(*.)+(spec|test).[jt]s?(x)"
- ],
+ // Define the configuration for `.astro` file.
+ "files": ["*.astro"],
+ // Allows Astro components to be parsed.
+ "parser": "astro-eslint-parser",
+ // Parse the script in `.astro` as TypeScript by adding the following configuration.
+ // It's the setting you need when using TypeScript.
+ "parserOptions": {
+ "parser": "@typescript-eslint/parser",
+ "extraFileExtensions": [".astro"]
+ },
+ "rules": {
+ // override/add rules settings here, such as:
+ // "astro/no-set-html-directive": "error"
+ }
+ },
+ {
+ "files": ["**/?(*.)+(spec|test).[jt]sx"],
"extends": ["plugin:testing-library/react"],
"rules": {
"testing-library/no-node-access": "off",
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b5cd9832..09e8a32f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -8,88 +8,137 @@ on:
branches:
- '**'
+env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ PUBLIC_TYPEKIT_ID: ${{ secrets.GATSBY_TYPEKIT_ID }}
+ PUBLIC_MAPBOX_ACCESS_TOKEN: ${{ secrets.GATSBY_MAPBOX_ACCESS_TOKEN }}
+ PUBLIC_UMAMI_SCRIPT_URL: ${{ secrets.GATSBY_UMAMI_SCRIPT_URL }}
+ PUBLIC_UMAMI_WEBSITE_ID: ${{ secrets.GATSBY_UMAMI_WEBSITE_ID }}
+ PUBLIC_INFURA_ID: ${{ secrets.GATSBY_INFURA_ID }}
+ PUBLIC_WALLETCONNECT_ID: ${{ secrets.GATSBY_WALLETCONNECT_ID }}
+
jobs:
- test:
- runs-on: ubuntu-latest
+ lint:
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, macos-13]
+ node: ['18']
+ runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
- node-version: '18'
-
- - name: Cache node modules
- uses: actions/cache@v3
- with:
- path: ~/.npm
- key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
- restore-keys: ${{ runner.os }}-node-
-
- - name: Cache Gatsby Cache Folder
- uses: actions/cache@v3
- with:
- path: .cache
- key: ${{ runner.os }}-cache-gatsby
- restore-keys: ${{ runner.os }}-cache-gatsby
-
- - name: Cache Gatsby Public Folder
- uses: actions/cache@v3
- with:
- path: public/
- key: ${{ runner.os }}-public-gatsby
- restore-keys: ${{ runner.os }}-public-gatsby
-
+ node-version: ${{ matrix.node }}
+ cache: 'npm'
- run: npm ci
- - run: npm run build
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- GATSBY_TYPEKIT_ID: ${{ secrets.GATSBY_TYPEKIT_ID }}
- GATSBY_MAPBOX_ACCESS_TOKEN: ${{ secrets.GATSBY_MAPBOX_ACCESS_TOKEN }}
- GATSBY_UMAMI_SCRIPT_URL: ${{ secrets.GATSBY_UMAMI_SCRIPT_URL }}
- GATSBY_UMAMI_WEBSITE_ID: ${{ secrets.GATSBY_UMAMI_WEBSITE_ID }}
- GATSBY_INFURA_ID: ${{ secrets.GATSBY_INFURA_ID }}
- - run: npm test
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- GATSBY_TYPEKIT_ID: ${{ secrets.GATSBY_TYPEKIT_ID }}
- GATSBY_MAPBOX_ACCESS_TOKEN: ${{ secrets.GATSBY_MAPBOX_ACCESS_TOKEN }}
- GATSBY_UMAMI_SCRIPT_URL: ${{ secrets.GATSBY_UMAMI_SCRIPT_URL }}
- GATSBY_UMAMI_WEBSITE_ID: ${{ secrets.GATSBY_UMAMI_WEBSITE_ID }}
- GATSBY_INFURA_ID: ${{ secrets.GATSBY_INFURA_ID }}
+ - run: npm run lint
+ typecheck:
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, macos-13]
+ node: ['18']
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-node@v3
+ with:
+ node-version: ${{ matrix.node }}
+ cache: 'npm'
+ - run: npm ci
+ - run: npm run prebuild
+ - run: npm run typecheck
+
+ test-unit:
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, macos-13]
+ node: ['18']
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-node@v3
+ with:
+ node-version: ${{ matrix.node }}
+ cache: 'npm'
+ - run: npm ci
+ - run: npm run test:unit
- uses: actions/upload-artifact@v3
with:
- name: coverage
+ name: coverage-${{ matrix.os }}-${{ matrix.node }}
path: coverage/
+ test-e2e:
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest]
+ node: ['18']
+ runs-on: ${{ matrix.os }}
+ timeout-minutes: 60
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-node@v3
+ with:
+ node-version: ${{ matrix.node }}
+ cache: 'npm'
+ - run: npm ci
+ - run: npx playwright install --with-deps
+ - run: npm run test:e2e
+
+ build:
+ strategy:
+ fail-fast: false
+ matrix:
+ os: [ubuntu-latest, macos-13]
+ node: ['18']
+ runs-on: ${{ matrix.os }}
+ steps:
+ - uses: actions/checkout@v3
+ - uses: actions/setup-node@v3
+ with:
+ node-version: ${{ matrix.node }}
+ cache: 'npm'
+ - name: Cache Astro build output
+ uses: actions/cache@v3
+ with:
+ path: ${{ github.workspace }}/.astro
+ key: ${{ matrix.os }}-${{ matrix.node }}-astro-build-${{ hashFiles('content/**/*.jpg', 'content/**/*.png') }}
+ restore-keys: ${{ matrix.os }}-${{ matrix.node }}-astro-build-
+ - run: npm ci
+ - run: npm run build
- uses: actions/upload-artifact@v1
if: github.ref == 'refs/heads/main'
with:
- name: public
- path: public
+ name: dist-${{ matrix.os }}-${{ matrix.node }}
+ path: dist
coverage:
runs-on: ubuntu-latest
- needs: [test]
+ needs: [test-unit]
if: ${{ success() && github.actor != 'dependabot[bot]' }}
steps:
- uses: actions/checkout@v3
- uses: actions/download-artifact@v3
with:
- name: coverage
- - uses: paambaati/codeclimate-action@v2.7.5
+ name: coverage-ubuntu-latest-18
+ - uses: paambaati/codeclimate-action@v5.0.0
env:
CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }}
deploy:
- needs: test
+ needs: [lint, typecheck, test-unit, test-e2e, build]
if: success() && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v3
- uses: actions/download-artifact@v1
with:
- name: public
+ name: dist-ubuntu-latest-18
- name: Deploy to S3
run: npm run deploy:s3
env:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index d1c2fe1c..830c03ed 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -32,7 +32,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- language: ['javascript']
+ language: ['javascript', 'typescript']
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://git.io/codeql-language-support
diff --git a/.gitignore b/.gitignore
index 700ef2b8..aec6908e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,26 @@
node_modules
.yarnclean
-public
.cache
-plugins/gatsby-redirect-from
coverage
.env
.env.development
.nova
-src/@types/Gatsby.d.ts
\ No newline at end of file
+
+# build output
+dist/
+src/images/components/
+
+# generated types
+.astro/
+
+# autogenerated stuff
+public/get/
+src/content/
+src/content
+public/post-kbd.css
+.config/redirects.json
+test-results/
+playwright-report/
+playwright/.cache/
+**/tmp/
+public/theme.js
diff --git a/.jest/__fixtures__/avatar.json b/.jest/__fixtures__/avatar.json
deleted file mode 100644
index dfd08cc5..00000000
--- a/.jest/__fixtures__/avatar.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "avatar": {
- "edges": [
- {
- "node": {
- "childImageSharp": {
- "fixed": {
- "aspectRatio": 1,
- "width": 80,
- "height": 80,
- "src": "/static/b61a09d5f4cbd9d8b2844091590ddea4/fce1d/avatar.jpg",
- "srcSet": "/static/b61a09d5f4cbd9d8b2844091590ddea4/fce1d/avatar.jpg 1x,\n/static/b61a09d5f4cbd9d8b2844091590ddea4/c4de8/avatar.jpg 1.5x,\n/static/b61a09d5f4cbd9d8b2844091590ddea4/c3234/avatar.jpg 2x",
- "srcWebp": "/static/b61a09d5f4cbd9d8b2844091590ddea4/ad6b8/avatar.webp",
- "srcSetWebp": "/static/b61a09d5f4cbd9d8b2844091590ddea4/ad6b8/avatar.webp 1x,\n/static/b61a09d5f4cbd9d8b2844091590ddea4/5c322/avatar.webp 1.5x,\n/static/b61a09d5f4cbd9d8b2844091590ddea4/5bf6b/avatar.webp 2x",
- "originalName": "avatar.jpg"
- }
- }
- }
- }
- ]
- }
-}
diff --git a/.jest/__fixtures__/github.json b/.jest/__fixtures__/github.json
deleted file mode 100644
index b29585d4..00000000
--- a/.jest/__fixtures__/github.json
+++ /dev/null
@@ -1,219 +0,0 @@
-{
- "github": {
- "viewer": {
- "repositories": {
- "edges": [
- {
- "node": {
- "name": "csspaperstack",
- "url": "https://github.com/kremalicious/csspaperstack",
- "owner": {
- "login": "kremalicious"
- },
- "object": null
- }
- },
- {
- "node": {
- "name": "hastuzeit",
- "url": "https://github.com/kremalicious/hastuzeit",
- "owner": {
- "login": "kremalicious"
- },
- "object": null
- }
- },
- {
- "node": {
- "name": "Badged",
- "url": "https://github.com/kremalicious/Badged",
- "owner": {
- "login": "kremalicious"
- },
- "object": null
- }
- },
- {
- "node": {
- "name": "hsresponsive",
- "url": "https://github.com/kremalicious/hsresponsive",
- "owner": {
- "login": "kremalicious"
- },
- "object": null
- }
- },
- {
- "node": {
- "name": "dribbble-chromeapp",
- "url": "https://github.com/kremalicious/dribbble-chromeapp",
- "owner": {
- "login": "kremalicious"
- },
- "object": null
- }
- },
- {
- "node": {
- "name": "krtmn",
- "url": "https://github.com/kremalicious/krtmn",
- "owner": {
- "login": "kremalicious"
- },
- "object": {
- "id": "MDQ6QmxvYjM2MzA0MzQ6NDcwYWRiOTc5MWVjYzZkNmU0YzMzNDQ3ZmZhZjBhNjA0ZGE1NTBkNA==",
- "text": "YOURLS Changelog\n================\n\n_This file lists the main changes through all versions of YOURLS. \nFor a much more detailed list, simply refer to [commit messages](https://github.com/YOURLS/YOURLS/commits/master)._\n\n1.7\n---\n- added: support for PDO and MySQLi\n- added: social bookmarklets - share on Twitter, Facebook or Tumblr in a click\n- added: check api.yourls.org if a new version of YOURLS is available\n- added: proxy support - install YOURLS behind a firewall!\n- improved: security regarding SQL injections\n- improved: security regarding your credentials - now auto-encrypted\n- improved: external HTTP request handling\n- improved: ƒυηкƴ UTF-8 titles handling\n- fixed: compatibility with Apache mod_security blocking bookmarklets\n- fixed: lots of bugs\n\n1.6\n---\n- added: مرحبا العالم! Hej verden! 你好世界! Kumusta mundo! Ciao mondo! Hello world! Translation API.\n- added: custom API actions\n- added: support for URLs with common protocols\n- fixed: search and pagination in the admin interface\n- updated: third party libs jQuery, ezSQL, GeoIP\n- improved: sanitizing and escaping functions\n\n1.5.1\n-----\n- added: full jsonp support\n- added: ability to use encrypted passwords in the config file\n- fixed: support for http://www.sho.rt/bleh and http://sho.rt/bleh\n- added: support for any favicon dropped in the /user directory\n- updated: Google Visualization API instead of deprecated Google Charts\n- fixed: bugs, bugs, bugs\n- added: hooks, hooks, hooks\n- improved: things, things, things\n\n1.5\n---\n- added: plugin architecture! OMG plugins!!1!!1!\n- added: directory /user, config.php can be moved there\n- added: new \"instant bookmarklets\"\n- added: 1 click copy-to-clipboard a la bitly\n- change in logic: now all request are handled by PHP and don't rely on .htaccess\n- added: saving URL titles\n- added: support for prefix-n-shorten: sho.rt/http://example.com/\n- added: core plugin to allow hyphens in URLs\n- added: core sample plugin to wrap redirected URLs in a social toolbar\n- added: core sample plugin to show how to create administration page in plugins\n- added: core plugin to display a random pretty background\n- changed: layout now using a more consistent palette, see http://yourls.org/palette\n- added: anti XSS and anti CSRF measures\n- added: interactive map if possible in stat traffic by countries\n- fixed: lots of bugs\n\n1.4.3\n-----\n- fixed bug no-stats-showing-ffs due to inconsistency in DB schema\n- improve error reporting with API method url-stat\n\n1.4.2\n-----\n- fixed: bug in auth function\n- added: sample public API file\n- added: check in API requests for WordPress plugin when adding a new short URL\n- prettier sample public interface\n\n1.4.1\n-----\n- fixed: base 62 URLs (keywords with MiXeD CaSe)\n- new & secure auth method for API calls, with no need to use login & password combo\n- allow SSL enforcement for admin pages\n- new API method: stats for individual URL.\n- prevent internal redirection loops\n- filter and search URLs & short URLs by date\n\n1.4\n---\n- added: an upgrader from 1.3 to 1.4\n- change in logic: now using a global object $ydb for everything related to DB and other globally needed stuff\n- change in logic: include \"load-yourls.php\" instead of \"config.php\" to start engine\n- change in DB schema: now storing URLs with their keyword as used in shorturl, allowing for any keyword length\n- change in DB schema: new table for storing various options including next_id, dropping table of the same name\n- change in DB schema: new table for storing hits (for stats)\n- improved the installer, with .htaccess file creation\n- layout tweak: now prettier, isn't it?\n- stats! OMG stats!\n\n1.3-RC1\n-------\n- added bookmarklet and tools page\n- improved XSS filter when adding new URL\n- code cleanup in admin/index.php to separate code and display\n- added favicon\n- stricter coding to prevent notices with undefined indexes\n- hide PHP notices & SQL errors & warnings, unless YOURLS_DEBUG constant set to true\n\n1.2\n---\n- don't remember. A few tiny stuff for sure.\n\n1.1\n---\n- don't remember. Some little bugs I guess.\n\n1.0.1\n-----\n- don't remember. Trivial stuff probably.\n\n1.0\n---\n- initial release\n"
- }
- }
- },
- {
- "node": {
- "name": "glassfruit",
- "url": "https://github.com/kremalicious/glassfruit",
- "owner": {
- "login": "kremalicious"
- },
- "object": null
- }
- },
- {
- "node": {
- "name": "kremalicious2",
- "url": "https://github.com/kremalicious/kremalicious2",
- "owner": {
- "login": "kremalicious"
- },
- "object": null
- }
- },
- {
- "node": {
- "name": "wp-icons-template",
- "url": "https://github.com/kremalicious/wp-icons-template",
- "owner": {
- "login": "kremalicious"
- },
- "object": null
- }
- },
- {
- "node": {
- "name": "kbdfun",
- "url": "https://github.com/kremalicious/kbdfun",
- "owner": {
- "login": "kremalicious"
- },
- "object": null
- }
- },
- {
- "node": {
- "name": "dotfiles",
- "url": "https://github.com/kremalicious/dotfiles",
- "owner": {
- "login": "kremalicious"
- },
- "object": null
- }
- },
- {
- "node": {
- "name": "kretschmann.io",
- "url": "https://github.com/kremalicious/kretschmann.io",
- "owner": {
- "login": "kremalicious"
- },
- "object": null
- }
- },
- {
- "node": {
- "name": "blog",
- "url": "https://github.com/kremalicious/blog",
- "owner": {
- "login": "kremalicious"
- },
- "object": null
- }
- },
- {
- "node": {
- "name": "files",
- "url": "https://github.com/kremalicious/files",
- "owner": {
- "login": "kremalicious"
- },
- "object": null
- }
- },
- {
- "node": {
- "name": "appstorebadges",
- "url": "https://github.com/kremalicious/appstorebadges",
- "owner": {
- "login": "kremalicious"
- },
- "object": null
- }
- },
- {
- "node": {
- "name": "hyper-mac-pro",
- "url": "https://github.com/kremalicious/hyper-mac-pro",
- "owner": {
- "login": "kremalicious"
- },
- "object": null
- }
- },
- {
- "node": {
- "name": "ipfs-hosting",
- "url": "https://github.com/kremalicious/ipfs-hosting",
- "owner": {
- "login": "kremalicious"
- },
- "object": null
- }
- },
- {
- "node": {
- "name": "gatsby-plugin-matomo",
- "url": "https://github.com/kremalicious/gatsby-plugin-matomo",
- "owner": {
- "login": "kremalicious"
- },
- "object": {
- "id": "MDQ6QmxvYjEzMjQ5NDg4NTozNzMwOWUxMTc4MTA5YjJmZDQ3ZDg5YTZlZjIwNjM5NTNlNDNlZjA1",
- "text": "### Changelog\n\nAll notable changes to this project will be documented in this file. Dates are displayed in UTC.\n\nGenerated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).\n\n#### [v0.7.2](https://github.com/kremalicious/gatsby-plugin-matomo/compare/v0.7.1...v0.7.2)\n\n> 13 June 2019\n\n- Fix missing tracking code in rendered HTML [`#18`](https://github.com/kremalicious/gatsby-plugin-matomo/pull/18)\n- update changelog [`57092e5`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/57092e5b0f755867885d4877ba539e2e684eba3c)\n- Release 0.7.2 [`31cabd6`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/31cabd60afbf9da39bce3cbd9187ba203c8f6e93)\n\n#### [v0.7.1](https://github.com/kremalicious/gatsby-plugin-matomo/compare/0.7.0...v0.7.1)\n\n> 10 June 2019\n\n- preconnect to configured Matomo host url [`#17`](https://github.com/kremalicious/gatsby-plugin-matomo/pull/17)\n- Add prettier [`#16`](https://github.com/kremalicious/gatsby-plugin-matomo/pull/16)\n- add prettier [`858fc2e`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/858fc2eb8a58dc49cfe1fea77d2b71c7d2ac4888)\n- new release-it setup [`bbb5f19`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/bbb5f19f94b867b7f7f6959a201690c290ac0fe2)\n- bump packages [`e3479f6`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/e3479f6955f6324140e7243b0f680dd3771250f7)\n\n#### [0.7.0](https://github.com/kremalicious/gatsby-plugin-matomo/compare/0.6.1...0.7.0)\n\n> 30 March 2019\n\n- Page tracking improvements [`#15`](https://github.com/kremalicious/gatsby-plugin-matomo/pull/15)\n- capture and send custom page title [`#11`](https://github.com/kremalicious/gatsby-plugin-matomo/issues/11)\n- better page url & referrer url tracking [`04b8ba1`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/04b8ba19038f49571a0f19099f9af4575739a8fb)\n- update changelog [`24f57da`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/24f57da352ee95781ed2c4268f31adaaa5aa1d82)\n- Release 0.7.0 [`4c36d9c`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/4c36d9c7c6212542fc1b3ebba977ee9c1e9bc526)\n\n#### [0.6.1](https://github.com/kremalicious/gatsby-plugin-matomo/compare/v0.6.0...0.6.1)\n\n> 30 March 2019\n\n- package updates [`#14`](https://github.com/kremalicious/gatsby-plugin-matomo/pull/14)\n- Update release-it to the latest version 🚀 [`#12`](https://github.com/kremalicious/gatsby-plugin-matomo/pull/12)\n- Update release-it to the latest version 🚀 [`#10`](https://github.com/kremalicious/gatsby-plugin-matomo/pull/10)\n- run changelog before release [`5da11a4`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/5da11a47609f48328d544bab07600576c6f060e0)\n- Release 0.6.1 [`f62c5bf`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/f62c5bf2926726bfc649d24b7fd555974c10482f)\n- chore(package): update release-it to version 10.0.0 [`d56c6c3`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/d56c6c3dba6607f5d67bf70a0953a8d922d84298)\n\n#### [v0.6.0](https://github.com/kremalicious/gatsby-plugin-matomo/compare/v0.5.1...v0.6.0)\n\n> 5 December 2018\n\n- Path exclusion [`#9`](https://github.com/kremalicious/gatsby-plugin-matomo/pull/9)\n- add changelog [`2b268a1`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/2b268a1c6c0df2b4e344c1f965a665e7be8d9341)\n- allow exclusion of paths [`9464d47`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/9464d47ae1b191ef494148fab4af200e3ea9c85c)\n- make release-it work with conventional-changelog [`2dcd5a7`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/2dcd5a7d02a2c35b95ddbec44d2319b0b25dc682)\n\n#### [v0.5.1](https://github.com/kremalicious/gatsby-plugin-matomo/compare/v0.5.0...v0.5.1)\n\n> 21 November 2018\n\n- Update release-it to the latest version 🚀 [`#8`](https://github.com/kremalicious/gatsby-plugin-matomo/pull/8)\n- bump packages [`cb26a40`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/cb26a40ece69e756862c5b6986eed60acca61bdd)\n- new Matomo logo [`64dd425`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/64dd4259ecda1fba49ac2bbfba7727bd5a544e3c)\n- Release 0.5.1 [`c442282`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/c44228275734a5edcf790a0bd8c91b9eec634891)\n\n#### [v0.5.0](https://github.com/kremalicious/gatsby-plugin-matomo/compare/v0.4.1...v0.5.0)\n\n> 6 August 2018\n\n- Add consent mode [`#7`](https://github.com/kremalicious/gatsby-plugin-matomo/pull/7)\n- Scoping js variables to avoid polluting global scope [`#6`](https://github.com/kremalicious/gatsby-plugin-matomo/pull/6)\n- bump packages [`b08b4a5`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/b08b4a5a45c477898b53831e48109bd672e3e11d)\n- add codeclimate config [`a96e8ea`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/a96e8ea5f89fdc07f84bdd49b927c5c757cf4665)\n- Add disableCookies option [`ff9a2c7`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/ff9a2c755867e60964953c109ab382915b662279)\n\n#### [v0.4.1](https://github.com/kremalicious/gatsby-plugin-matomo/compare/v0.4.0...v0.4.1)\n\n> 28 June 2018\n\n- Update eslint to the latest version 🚀 [`#5`](https://github.com/kremalicious/gatsby-plugin-matomo/pull/5)\n- improvements for SPA tracking [`7d653d5`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/7d653d564cb0ca4ae05d6897d94185ea2198462e)\n- package updates [`347f951`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/347f951174ec6b9f31b91cbd7bce90178eb49a1a)\n- Release 0.4.1 [`b254c13`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/b254c130b9ee8c750e2dd3f69540537094eac235)\n\n#### [v0.4.0](https://github.com/kremalicious/gatsby-plugin-matomo/compare/v0.3.2...v0.4.0)\n\n> 19 June 2018\n\n- Updates for Gatsby v2 [`#4`](https://github.com/kremalicious/gatsby-plugin-matomo/pull/4)\n- updates for Gatsby v2 [`59e442a`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/59e442a31d85af27f8cdf65a2d15ed8601995fad)\n- package updates [`356e68f`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/356e68faf500baf4c69461fa8323995f30400e24)\n- Release 0.4.0 [`1173236`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/117323688af1963ca5602b13ddd6131aba70376c)\n\n#### [v0.3.2](https://github.com/kremalicious/gatsby-plugin-matomo/compare/v0.3.1...v0.3.2)\n\n> 23 May 2018\n\n- save some lines [`5034a3b`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/5034a3b8e5201d976f7deb210b471caa50ea16cd)\n- Release 0.3.2 [`c531e47`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/c531e47cf5905acda149a637606ecfea9e2e40bc)\n- change repository field [`260273e`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/260273e0139db6d5bf76bc29c37d2f93fd254f05)\n\n#### [v0.3.1](https://github.com/kremalicious/gatsby-plugin-matomo/compare/v0.3.0...v0.3.1)\n\n> 14 May 2018\n\n- package updates [`2acbd45`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/2acbd450cb7dffe39c3cb57edbad5aeebe6d7799)\n- Release 0.3.1 [`f9050ce`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/f9050ceaa2b8d214d113ca78785bb7ba78f14d5a)\n- fix environment check [`dc574ab`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/dc574ab72c4ec01c4fa995ae9aa9a1469f04387a)\n\n#### [v0.3.0](https://github.com/kremalicious/gatsby-plugin-matomo/compare/v0.2.0...v0.3.0)\n\n> 10 May 2018\n\n- allow local piwik.js path to be set [`a15146c`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/a15146c3dc6b692070e09b38646ad925fc92fb28)\n- Release 0.3.0 [`2e36f13`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/2e36f1383d39703d10405b9e82782a6efa54d56f)\n\n#### [v0.2.0](https://github.com/kremalicious/gatsby-plugin-matomo/compare/v0.1.3...v0.2.0)\n\n> 8 May 2018\n\n- add dev mode [`5c05efe`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/5c05efe1815fb617e0153924696dc6350e2e753c)\n- Release 0.2.0 [`a3ef6d4`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/a3ef6d4c635a15d6cc64c7c3735f551737f3eff7)\n\n#### [v0.1.3](https://github.com/kremalicious/gatsby-plugin-matomo/compare/v0.1.2...v0.1.3)\n\n> 8 May 2018\n\n- readme updates [`49640f4`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/49640f4d37b54728c5a80ae9f2c63cb103318386)\n- Release 0.1.3 [`0109e19`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/0109e19e106f8620056db2775205101bf4cc1c3b)\n\n#### [v0.1.2](https://github.com/kremalicious/gatsby-plugin-matomo/compare/v0.1.1...v0.1.2)\n\n> 8 May 2018\n\n- Add Greenkeeper badge 🌴 [`#1`](https://github.com/kremalicious/gatsby-plugin-matomo/pull/1)\n- do nothing on route updates when piwik isn't loaded and in development [`6c0a840`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/6c0a840de21ce46f544b6ce9a7dd63e961e7ab6b)\n- move greenskeeper badge [`b361eef`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/b361eef43bc85c7acf8be73f3067686912ac49b3)\n- Release 0.1.2 [`370025f`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/370025f1ee733d8f87a70def3bf213e52a1125fa)\n\n#### [v0.1.1](https://github.com/kremalicious/gatsby-plugin-matomo/compare/v0.1.0...v0.1.1)\n\n> 7 May 2018\n\n- link up readme badges [`6793592`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/6793592ed28e23177ad0450874ebba055d912dc2)\n- Release 0.1.1 [`48c73ff`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/48c73ff2415dd25ce85924cdd53056b8f561face)\n- docs(readme): add Greenkeeper badge [`ec73329`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/ec73329dd2ad383aa380ed57667da7a7019824a1)\n\n#### v0.1.0\n\n> 7 May 2018\n\n- initial commit 🍹 [`49bffd3`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/49bffd3cf18f1ba9e099048f9a5591f9200f3296)\n- add Travis, add badges, add semi-auto releases [`fe823b7`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/fe823b76a18a5310e3cd19965b9f61e02774f7e0)\n- Release 0.1.0 [`09739a7`](https://github.com/kremalicious/gatsby-plugin-matomo/commit/09739a72c5c2e08b0a74faba8bc4cfb03fdac220)\n"
- }
- }
- },
- {
- "node": {
- "name": "portfolio",
- "url": "https://github.com/kremalicious/portfolio",
- "owner": {
- "login": "kremalicious"
- },
- "object": null
- }
- },
- {
- "node": {
- "name": "gatsby-redirect-from",
- "url": "https://github.com/kremalicious/gatsby-redirect-from",
- "owner": {
- "login": "kremalicious"
- },
- "object": {
- "id": "MDQ6QmxvYjE0NjY2ODk3NDo1YmVmMzg0N2ZlZThjMTk3NThiYThmMjBiMzE3NDFmN2I0YTcwZGU4",
- "text": "### Changelog\n\nAll notable changes to this project will be documented in this file. Dates are displayed in UTC.\n\nGenerated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).\n\n#### [v0.2.1](https://github.com/kremalicious/gatsby-redirect-from/compare/v0.2.0...v0.2.1)\n\n> 10 June 2019\n\n- fix auto changelog [`bba9f39`](https://github.com/kremalicious/gatsby-redirect-from/commit/bba9f3915511193384863b61b28b3c7f36d1007f)\n\n#### [v0.2.0](https://github.com/kremalicious/gatsby-redirect-from/compare/v0.1.1...v0.2.0)\n\n> 10 June 2019\n\n- Update docs & dependencies [`#12`](https://github.com/kremalicious/gatsby-redirect-from/pull/12)\n- specify node 8 instead of 10 as engine [`#11`](https://github.com/kremalicious/gatsby-redirect-from/pull/11)\n- Support alternative markdown queries (such as allMdx) [`#10`](https://github.com/kremalicious/gatsby-redirect-from/pull/10)\n- Update release-it to the latest version 🚀 [`#8`](https://github.com/kremalicious/gatsby-redirect-from/pull/8)\n- update docs, mention new options [`07835d8`](https://github.com/kremalicious/gatsby-redirect-from/commit/07835d8405854a4e5a190a6eeeb252604aaaddb7)\n- simplify release-it tasks, add changelog [`3ec4ccf`](https://github.com/kremalicious/gatsby-redirect-from/commit/3ec4ccfb8e19032068fd34c24929179a5237f0ca)\n- add prettier [`3f33f65`](https://github.com/kremalicious/gatsby-redirect-from/commit/3f33f65430126e634f498629e6be8437c6f1ed7b)\n\n#### [v0.1.1](https://github.com/kremalicious/gatsby-redirect-from/compare/v0.1.0...v0.1.1)\n\n> 30 August 2018\n\n- integrate with gatsby-plugin-meta-redirect, notes on server rendering [`2cd07ee`](https://github.com/kremalicious/gatsby-redirect-from/commit/2cd07ee33e2c267b3783630c6e5cfbdb6fb57c4b)\n- Release 0.1.1 [`e72d5ab`](https://github.com/kremalicious/gatsby-redirect-from/commit/e72d5aba5474104d34c99fcc5880a39c02696918)\n\n#### [v0.1.0](https://github.com/kremalicious/gatsby-redirect-from/compare/v0.0.3...v0.1.0)\n\n> 30 August 2018\n\n- babelify build [`#5`](https://github.com/kremalicious/gatsby-redirect-from/pull/5)\n- clarify prerequisites [`139a4af`](https://github.com/kremalicious/gatsby-redirect-from/commit/139a4afc1d81fee444a6bf0c4775c63aea4e7c09)\n- Release 0.1.0 [`9bd28a7`](https://github.com/kremalicious/gatsby-redirect-from/commit/9bd28a7e9cc3cfa83e3b5b7606b08d3cb26fafe0)\n- add npm badge [`53043c8`](https://github.com/kremalicious/gatsby-redirect-from/commit/53043c82f86dfd2e17c1ca0ec4580441baeb6d8d)\n\n#### [v0.0.3](https://github.com/kremalicious/gatsby-redirect-from/compare/v0.0.2...v0.0.3)\n\n> 29 August 2018\n\n- fix publish [`c91a362`](https://github.com/kremalicious/gatsby-redirect-from/commit/c91a3626390bfa632322007300818e58a25d75d0)\n- Release 0.0.3 [`f1d4982`](https://github.com/kremalicious/gatsby-redirect-from/commit/f1d4982d2424beb67119702a8181d4284ffe399e)\n\n#### v0.0.2\n\n> 29 August 2018\n\n- Add Greenkeeper badge 🌴 [`#1`](https://github.com/kremalicious/gatsby-redirect-from/pull/1)\n- initial commit 🍷 [`dfb3bb0`](https://github.com/kremalicious/gatsby-redirect-from/commit/dfb3bb0954b1933c4d9e36ebc8cb302a0dc70795)\n- Release 0.0.2 [`6924e7b`](https://github.com/kremalicious/gatsby-redirect-from/commit/6924e7b2d9d00a6eb6a03eeb328f29d561dbf904)\n- fix test [`b0129a5`](https://github.com/kremalicious/gatsby-redirect-from/commit/b0129a5ee73d292422a2f7746412aa55a0ce1097)\n"
- }
- }
- }
- ]
- }
- }
- }
-}
diff --git a/.jest/__fixtures__/home.json b/.jest/__fixtures__/home.json
deleted file mode 100644
index 46be0217..00000000
--- a/.jest/__fixtures__/home.json
+++ /dev/null
@@ -1,523 +0,0 @@
-{
- "latestArticles": {
- "edges": [
- {
- "node": {
- "id": "108cdbf6-2442-54b5-887c-aad821fba6c2",
- "fileAbsolutePath": "/Users/m/Code/blog/content/articles/2020-05-22-gatsby-redirect-from/index.md",
- "frontmatter": {
- "title": "Redirect plugin for Markdown Pages in Gatsby",
- "linkurl": null,
- "updated": "2020-05-23T09:35:12.000Z",
- "image": {
- "childImageSharp": {
- "original": {
- "src": "/static/gatsby-redirect-from-teaser-0d31de44f593361b78d2af6a9c030de3.png"
- },
- "fluid": {
- "aspectRatio": 3,
- "src": "/static/0d31de44f593361b78d2af6a9c030de3/7547c/gatsby-redirect-from-teaser.png",
- "srcSet": "/static/0d31de44f593361b78d2af6a9c030de3/f353c/gatsby-redirect-from-teaser.png 105w,\n/static/0d31de44f593361b78d2af6a9c030de3/45430/gatsby-redirect-from-teaser.png 210w,\n/static/0d31de44f593361b78d2af6a9c030de3/7547c/gatsby-redirect-from-teaser.png 420w,\n/static/0d31de44f593361b78d2af6a9c030de3/4f753/gatsby-redirect-from-teaser.png 630w,\n/static/0d31de44f593361b78d2af6a9c030de3/60369/gatsby-redirect-from-teaser.png 840w,\n/static/0d31de44f593361b78d2af6a9c030de3/6364d/gatsby-redirect-from-teaser.png 1880w",
- "srcWebp": "/static/0d31de44f593361b78d2af6a9c030de3/8a0fd/gatsby-redirect-from-teaser.webp",
- "srcSetWebp": "/static/0d31de44f593361b78d2af6a9c030de3/2aec1/gatsby-redirect-from-teaser.webp 105w,\n/static/0d31de44f593361b78d2af6a9c030de3/74e55/gatsby-redirect-from-teaser.webp 210w,\n/static/0d31de44f593361b78d2af6a9c030de3/8a0fd/gatsby-redirect-from-teaser.webp 420w,\n/static/0d31de44f593361b78d2af6a9c030de3/70212/gatsby-redirect-from-teaser.webp 630w,\n/static/0d31de44f593361b78d2af6a9c030de3/a9b7e/gatsby-redirect-from-teaser.webp 840w,\n/static/0d31de44f593361b78d2af6a9c030de3/b6972/gatsby-redirect-from-teaser.webp 1880w",
- "sizes": "(max-width: 420px) 100vw, 420px"
- }
- }
- },
- "tags": ["goodies", "gatsby", "development"]
- },
- "fields": {
- "slug": "/gatsby-redirect-from",
- "date": "2020-05-22T14:08:00.367Z",
- "type": "article"
- }
- }
- },
- {
- "node": {
- "id": "cf701f60-bbb5-544f-bb13-97aa884b7ad7",
- "fileAbsolutePath": "/Users/m/Code/blog/content/articles/2020-05-08-uses/index.md",
- "frontmatter": {
- "title": "/uses",
- "linkurl": null,
- "updated": "2020-07-30T13:58:12.000Z",
- "image": {
- "childImageSharp": {
- "original": {
- "src": "/static/uses-teaser-dfb23ab7203b0f61b838c98f23ea7852.png"
- },
- "fluid": {
- "aspectRatio": 3,
- "src": "/static/dfb23ab7203b0f61b838c98f23ea7852/7547c/uses-teaser.png",
- "srcSet": "/static/dfb23ab7203b0f61b838c98f23ea7852/f353c/uses-teaser.png 105w,\n/static/dfb23ab7203b0f61b838c98f23ea7852/45430/uses-teaser.png 210w,\n/static/dfb23ab7203b0f61b838c98f23ea7852/7547c/uses-teaser.png 420w,\n/static/dfb23ab7203b0f61b838c98f23ea7852/4f753/uses-teaser.png 630w,\n/static/dfb23ab7203b0f61b838c98f23ea7852/60369/uses-teaser.png 840w,\n/static/dfb23ab7203b0f61b838c98f23ea7852/6364d/uses-teaser.png 1880w",
- "srcWebp": "/static/dfb23ab7203b0f61b838c98f23ea7852/8a0fd/uses-teaser.webp",
- "srcSetWebp": "/static/dfb23ab7203b0f61b838c98f23ea7852/2aec1/uses-teaser.webp 105w,\n/static/dfb23ab7203b0f61b838c98f23ea7852/74e55/uses-teaser.webp 210w,\n/static/dfb23ab7203b0f61b838c98f23ea7852/8a0fd/uses-teaser.webp 420w,\n/static/dfb23ab7203b0f61b838c98f23ea7852/70212/uses-teaser.webp 630w,\n/static/dfb23ab7203b0f61b838c98f23ea7852/a9b7e/uses-teaser.webp 840w,\n/static/dfb23ab7203b0f61b838c98f23ea7852/b6972/uses-teaser.webp 1880w",
- "sizes": "(max-width: 420px) 100vw, 420px"
- }
- }
- },
- "tags": [
- "personal",
- "macos",
- "ios",
- "mac",
- "iphone",
- "design",
- "development"
- ]
- },
- "fields": {
- "slug": "/uses",
- "date": "2020-05-10T21:51:12.000Z",
- "type": "article"
- }
- }
- },
- {
- "node": {
- "id": "d7662b67-4a20-52af-a847-9c4debcf1cec",
- "fileAbsolutePath": "/Users/m/Code/blog/content/articles/2020-03-04-raspberry-pi-file-and-screen-sharing-macos-ios/index.md",
- "frontmatter": {
- "title": "Setup Raspberry Pi File and Screen Sharing for macOS & iOS",
- "linkurl": null,
- "updated": null,
- "image": {
- "childImageSharp": {
- "original": {
- "src": "/static/raspberry-pi-file-and-screen-sharing-macos-ios-teaser-a5f639b239acd8c475ff7dd3844508e8.png"
- },
- "fluid": {
- "aspectRatio": 3,
- "src": "/static/a5f639b239acd8c475ff7dd3844508e8/7547c/raspberry-pi-file-and-screen-sharing-macos-ios-teaser.png",
- "srcSet": "/static/a5f639b239acd8c475ff7dd3844508e8/f353c/raspberry-pi-file-and-screen-sharing-macos-ios-teaser.png 105w,\n/static/a5f639b239acd8c475ff7dd3844508e8/45430/raspberry-pi-file-and-screen-sharing-macos-ios-teaser.png 210w,\n/static/a5f639b239acd8c475ff7dd3844508e8/7547c/raspberry-pi-file-and-screen-sharing-macos-ios-teaser.png 420w,\n/static/a5f639b239acd8c475ff7dd3844508e8/4f753/raspberry-pi-file-and-screen-sharing-macos-ios-teaser.png 630w,\n/static/a5f639b239acd8c475ff7dd3844508e8/60369/raspberry-pi-file-and-screen-sharing-macos-ios-teaser.png 840w,\n/static/a5f639b239acd8c475ff7dd3844508e8/6364d/raspberry-pi-file-and-screen-sharing-macos-ios-teaser.png 1880w",
- "srcWebp": "/static/a5f639b239acd8c475ff7dd3844508e8/8a0fd/raspberry-pi-file-and-screen-sharing-macos-ios-teaser.webp",
- "srcSetWebp": "/static/a5f639b239acd8c475ff7dd3844508e8/2aec1/raspberry-pi-file-and-screen-sharing-macos-ios-teaser.webp 105w,\n/static/a5f639b239acd8c475ff7dd3844508e8/74e55/raspberry-pi-file-and-screen-sharing-macos-ios-teaser.webp 210w,\n/static/a5f639b239acd8c475ff7dd3844508e8/8a0fd/raspberry-pi-file-and-screen-sharing-macos-ios-teaser.webp 420w,\n/static/a5f639b239acd8c475ff7dd3844508e8/70212/raspberry-pi-file-and-screen-sharing-macos-ios-teaser.webp 630w,\n/static/a5f639b239acd8c475ff7dd3844508e8/a9b7e/raspberry-pi-file-and-screen-sharing-macos-ios-teaser.webp 840w,\n/static/a5f639b239acd8c475ff7dd3844508e8/b6972/raspberry-pi-file-and-screen-sharing-macos-ios-teaser.webp 1880w",
- "sizes": "(max-width: 420px) 100vw, 420px"
- }
- }
- },
- "tags": [
- "macos",
- "ios",
- "linux",
- "raspberrypi",
- "ubuntu",
- "tutorial",
- "avahi"
- ]
- },
- "fields": {
- "slug": "/raspberry-pi-file-and-screen-sharing-macos-ios",
- "date": "2020-03-04T18:35:26.815Z",
- "type": "article"
- }
- }
- },
- {
- "node": {
- "id": "9acfbf40-a90a-56c3-ab08-b98be93bcd9a",
- "fileAbsolutePath": "/Users/m/Code/blog/content/articles/2019-10-24-ocean-protocol-and-ipfs-sitting-in-the-merkle-tree/index.md",
- "frontmatter": {
- "title": "Ocean Protocol and IPFS, Sitting In The Merkle Tree",
- "linkurl": null,
- "updated": null,
- "image": {
- "childImageSharp": {
- "original": {
- "src": "/static/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser-f724bdaae38e81ec90b01ec6b8412cc8.png"
- },
- "fluid": {
- "aspectRatio": 3,
- "src": "/static/f724bdaae38e81ec90b01ec6b8412cc8/7547c/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png",
- "srcSet": "/static/f724bdaae38e81ec90b01ec6b8412cc8/f353c/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png 105w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/45430/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png 210w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/7547c/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png 420w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/4f753/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png 630w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/60369/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png 840w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/766ad/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png 2000w",
- "srcWebp": "/static/f724bdaae38e81ec90b01ec6b8412cc8/8a0fd/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.webp",
- "srcSetWebp": "/static/f724bdaae38e81ec90b01ec6b8412cc8/2aec1/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.webp 105w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/74e55/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.webp 210w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/8a0fd/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.webp 420w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/70212/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.webp 630w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/a9b7e/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.webp 840w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/52dbc/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.webp 2000w",
- "sizes": "(max-width: 420px) 100vw, 420px"
- }
- }
- },
- "tags": [
- "oceanprotocol",
- "blockchain",
- "design",
- "development",
- "ipfs",
- "web3"
- ]
- },
- "fields": {
- "slug": "/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree",
- "date": "2019-10-24T00:00:00.000Z",
- "type": "article"
- }
- }
- },
- {
- "node": {
- "id": "45d2cb0a-5f77-5726-86d7-72b38b6c8024",
- "fileAbsolutePath": "/Users/m/Code/blog/content/articles/2019-07-18-the-commons-marketplace-in-pacific-network/index.md",
- "frontmatter": {
- "title": "The Commons Marketplace in Pacific Network",
- "linkurl": null,
- "updated": null,
- "image": {
- "childImageSharp": {
- "original": {
- "src": "/static/the-commons-marketplace-in-pacific-network-teaser-2db57438c9f98018d8b61d9224d3fb77.png"
- },
- "fluid": {
- "aspectRatio": 3,
- "src": "/static/2db57438c9f98018d8b61d9224d3fb77/7547c/the-commons-marketplace-in-pacific-network-teaser.png",
- "srcSet": "/static/2db57438c9f98018d8b61d9224d3fb77/f353c/the-commons-marketplace-in-pacific-network-teaser.png 105w,\n/static/2db57438c9f98018d8b61d9224d3fb77/45430/the-commons-marketplace-in-pacific-network-teaser.png 210w,\n/static/2db57438c9f98018d8b61d9224d3fb77/7547c/the-commons-marketplace-in-pacific-network-teaser.png 420w,\n/static/2db57438c9f98018d8b61d9224d3fb77/4f753/the-commons-marketplace-in-pacific-network-teaser.png 630w,\n/static/2db57438c9f98018d8b61d9224d3fb77/60369/the-commons-marketplace-in-pacific-network-teaser.png 840w,\n/static/2db57438c9f98018d8b61d9224d3fb77/fe119/the-commons-marketplace-in-pacific-network-teaser.png 2762w",
- "srcWebp": "/static/2db57438c9f98018d8b61d9224d3fb77/8a0fd/the-commons-marketplace-in-pacific-network-teaser.webp",
- "srcSetWebp": "/static/2db57438c9f98018d8b61d9224d3fb77/2aec1/the-commons-marketplace-in-pacific-network-teaser.webp 105w,\n/static/2db57438c9f98018d8b61d9224d3fb77/74e55/the-commons-marketplace-in-pacific-network-teaser.webp 210w,\n/static/2db57438c9f98018d8b61d9224d3fb77/8a0fd/the-commons-marketplace-in-pacific-network-teaser.webp 420w,\n/static/2db57438c9f98018d8b61d9224d3fb77/70212/the-commons-marketplace-in-pacific-network-teaser.webp 630w,\n/static/2db57438c9f98018d8b61d9224d3fb77/a9b7e/the-commons-marketplace-in-pacific-network-teaser.webp 840w,\n/static/2db57438c9f98018d8b61d9224d3fb77/dc0ca/the-commons-marketplace-in-pacific-network-teaser.webp 2762w",
- "sizes": "(max-width: 420px) 100vw, 420px"
- }
- }
- },
- "tags": [
- "oceanprotocol",
- "blockchain",
- "design",
- "development",
- "web3"
- ]
- },
- "fields": {
- "slug": "/the-commons-marketplace-in-pacific-network",
- "date": "2019-07-18T00:00:00.000Z",
- "type": "article"
- }
- }
- }
- ]
- },
- "latestPhotos": {
- "edges": [
- {
- "node": {
- "id": "5f4558ee-381e-5fb0-9c0e-b60115b6e22e",
- "frontmatter": {
- "title": "The Light That Never Goes Out",
- "image": {
- "childImageSharp": {
- "original": {
- "src": "/static/2020-02-14-the-light-that-never-goes-out-a389a64d6f51e45fa8e0a7ea98f85fa4.jpg"
- },
- "fluid": {
- "aspectRatio": 1,
- "src": "/static/a389a64d6f51e45fa8e0a7ea98f85fa4/0f41b/2020-02-14-the-light-that-never-goes-out.jpg",
- "srcSet": "/static/a389a64d6f51e45fa8e0a7ea98f85fa4/73124/2020-02-14-the-light-that-never-goes-out.jpg 75w,\n/static/a389a64d6f51e45fa8e0a7ea98f85fa4/712e7/2020-02-14-the-light-that-never-goes-out.jpg 150w,\n/static/a389a64d6f51e45fa8e0a7ea98f85fa4/0f41b/2020-02-14-the-light-that-never-goes-out.jpg 300w,\n/static/a389a64d6f51e45fa8e0a7ea98f85fa4/270e8/2020-02-14-the-light-that-never-goes-out.jpg 450w,\n/static/a389a64d6f51e45fa8e0a7ea98f85fa4/aa95b/2020-02-14-the-light-that-never-goes-out.jpg 600w,\n/static/a389a64d6f51e45fa8e0a7ea98f85fa4/37b83/2020-02-14-the-light-that-never-goes-out.jpg 2772w",
- "srcWebp": "/static/a389a64d6f51e45fa8e0a7ea98f85fa4/40e2f/2020-02-14-the-light-that-never-goes-out.webp",
- "srcSetWebp": "/static/a389a64d6f51e45fa8e0a7ea98f85fa4/37dc9/2020-02-14-the-light-that-never-goes-out.webp 75w,\n/static/a389a64d6f51e45fa8e0a7ea98f85fa4/37a2a/2020-02-14-the-light-that-never-goes-out.webp 150w,\n/static/a389a64d6f51e45fa8e0a7ea98f85fa4/40e2f/2020-02-14-the-light-that-never-goes-out.webp 300w,\n/static/a389a64d6f51e45fa8e0a7ea98f85fa4/6a707/2020-02-14-the-light-that-never-goes-out.webp 450w,\n/static/a389a64d6f51e45fa8e0a7ea98f85fa4/bf714/2020-02-14-the-light-that-never-goes-out.webp 600w,\n/static/a389a64d6f51e45fa8e0a7ea98f85fa4/0a795/2020-02-14-the-light-that-never-goes-out.webp 2772w",
- "sizes": "(max-width: 300px) 100vw, 300px"
- }
- }
- }
- },
- "fields": {
- "slug": "/the-light-that-never-goes-out",
- "type": "photo"
- }
- }
- },
- {
- "node": {
- "id": "8c2d7d7e-3799-5f1e-8499-922d58a636ee",
- "frontmatter": {
- "title": "Balloon Dog",
- "image": {
- "childImageSharp": {
- "original": {
- "src": "/static/2020-01-17-balloon-dog-28916b152705a4f518c85519b6c1822b.jpg"
- },
- "fluid": {
- "aspectRatio": 1,
- "src": "/static/28916b152705a4f518c85519b6c1822b/0f41b/2020-01-17-balloon-dog.jpg",
- "srcSet": "/static/28916b152705a4f518c85519b6c1822b/73124/2020-01-17-balloon-dog.jpg 75w,\n/static/28916b152705a4f518c85519b6c1822b/712e7/2020-01-17-balloon-dog.jpg 150w,\n/static/28916b152705a4f518c85519b6c1822b/0f41b/2020-01-17-balloon-dog.jpg 300w,\n/static/28916b152705a4f518c85519b6c1822b/270e8/2020-01-17-balloon-dog.jpg 450w,\n/static/28916b152705a4f518c85519b6c1822b/aa95b/2020-01-17-balloon-dog.jpg 600w,\n/static/28916b152705a4f518c85519b6c1822b/d5887/2020-01-17-balloon-dog.jpg 2792w",
- "srcWebp": "/static/28916b152705a4f518c85519b6c1822b/40e2f/2020-01-17-balloon-dog.webp",
- "srcSetWebp": "/static/28916b152705a4f518c85519b6c1822b/37dc9/2020-01-17-balloon-dog.webp 75w,\n/static/28916b152705a4f518c85519b6c1822b/37a2a/2020-01-17-balloon-dog.webp 150w,\n/static/28916b152705a4f518c85519b6c1822b/40e2f/2020-01-17-balloon-dog.webp 300w,\n/static/28916b152705a4f518c85519b6c1822b/6a707/2020-01-17-balloon-dog.webp 450w,\n/static/28916b152705a4f518c85519b6c1822b/bf714/2020-01-17-balloon-dog.webp 600w,\n/static/28916b152705a4f518c85519b6c1822b/57701/2020-01-17-balloon-dog.webp 2792w",
- "sizes": "(max-width: 300px) 100vw, 300px"
- }
- }
- }
- },
- "fields": {
- "slug": "/balloon-dog",
- "type": "photo"
- }
- }
- },
- {
- "node": {
- "id": "a1cd842c-4755-508e-9709-2daabeb783a4",
- "frontmatter": {
- "title": "Bremen Cathedral",
- "image": {
- "childImageSharp": {
- "original": {
- "src": "/static/2020-01-17-bremen-cathedral-7203580f941ea9a9b0a27d6f41686724.jpg"
- },
- "fluid": {
- "aspectRatio": 1,
- "src": "/static/7203580f941ea9a9b0a27d6f41686724/0f41b/2020-01-17-bremen-cathedral.jpg",
- "srcSet": "/static/7203580f941ea9a9b0a27d6f41686724/73124/2020-01-17-bremen-cathedral.jpg 75w,\n/static/7203580f941ea9a9b0a27d6f41686724/712e7/2020-01-17-bremen-cathedral.jpg 150w,\n/static/7203580f941ea9a9b0a27d6f41686724/0f41b/2020-01-17-bremen-cathedral.jpg 300w,\n/static/7203580f941ea9a9b0a27d6f41686724/270e8/2020-01-17-bremen-cathedral.jpg 450w,\n/static/7203580f941ea9a9b0a27d6f41686724/aa95b/2020-01-17-bremen-cathedral.jpg 600w,\n/static/7203580f941ea9a9b0a27d6f41686724/ffda5/2020-01-17-bremen-cathedral.jpg 3115w",
- "srcWebp": "/static/7203580f941ea9a9b0a27d6f41686724/40e2f/2020-01-17-bremen-cathedral.webp",
- "srcSetWebp": "/static/7203580f941ea9a9b0a27d6f41686724/37dc9/2020-01-17-bremen-cathedral.webp 75w,\n/static/7203580f941ea9a9b0a27d6f41686724/37a2a/2020-01-17-bremen-cathedral.webp 150w,\n/static/7203580f941ea9a9b0a27d6f41686724/40e2f/2020-01-17-bremen-cathedral.webp 300w,\n/static/7203580f941ea9a9b0a27d6f41686724/6a707/2020-01-17-bremen-cathedral.webp 450w,\n/static/7203580f941ea9a9b0a27d6f41686724/bf714/2020-01-17-bremen-cathedral.webp 600w,\n/static/7203580f941ea9a9b0a27d6f41686724/ba27b/2020-01-17-bremen-cathedral.webp 3115w",
- "sizes": "(max-width: 300px) 100vw, 300px"
- }
- }
- }
- },
- "fields": {
- "slug": "/bremen-cathedral",
- "type": "photo"
- }
- }
- },
- {
- "node": {
- "id": "82a3ca09-ff38-5c2a-8b4f-6f5150d9448e",
- "frontmatter": {
- "title": "RAW Gelände",
- "image": {
- "childImageSharp": {
- "original": {
- "src": "/static/2020-01-05-raw-gelande-6d40de77304f8a335d88447d16125e5a.jpg"
- },
- "fluid": {
- "aspectRatio": 1,
- "src": "/static/6d40de77304f8a335d88447d16125e5a/0f41b/2020-01-05-raw-gelande.jpg",
- "srcSet": "/static/6d40de77304f8a335d88447d16125e5a/73124/2020-01-05-raw-gelande.jpg 75w,\n/static/6d40de77304f8a335d88447d16125e5a/712e7/2020-01-05-raw-gelande.jpg 150w,\n/static/6d40de77304f8a335d88447d16125e5a/0f41b/2020-01-05-raw-gelande.jpg 300w,\n/static/6d40de77304f8a335d88447d16125e5a/270e8/2020-01-05-raw-gelande.jpg 450w,\n/static/6d40de77304f8a335d88447d16125e5a/aa95b/2020-01-05-raw-gelande.jpg 600w,\n/static/6d40de77304f8a335d88447d16125e5a/30037/2020-01-05-raw-gelande.jpg 3024w",
- "srcWebp": "/static/6d40de77304f8a335d88447d16125e5a/40e2f/2020-01-05-raw-gelande.webp",
- "srcSetWebp": "/static/6d40de77304f8a335d88447d16125e5a/37dc9/2020-01-05-raw-gelande.webp 75w,\n/static/6d40de77304f8a335d88447d16125e5a/37a2a/2020-01-05-raw-gelande.webp 150w,\n/static/6d40de77304f8a335d88447d16125e5a/40e2f/2020-01-05-raw-gelande.webp 300w,\n/static/6d40de77304f8a335d88447d16125e5a/6a707/2020-01-05-raw-gelande.webp 450w,\n/static/6d40de77304f8a335d88447d16125e5a/bf714/2020-01-05-raw-gelande.webp 600w,\n/static/6d40de77304f8a335d88447d16125e5a/859d2/2020-01-05-raw-gelande.webp 3024w",
- "sizes": "(max-width: 300px) 100vw, 300px"
- }
- }
- }
- },
- "fields": {
- "slug": "/raw-gelande",
- "type": "photo"
- }
- }
- },
- {
- "node": {
- "id": "7c499977-3837-54bd-a8e9-945fb0e5c04a",
- "frontmatter": {
- "title": "Helmut Newton Foundation",
- "image": {
- "childImageSharp": {
- "original": {
- "src": "/static/2019-11-16-helmut-newton-foundation-d5cf288572434a487b055397b4cec51b.jpg"
- },
- "fluid": {
- "aspectRatio": 1,
- "src": "/static/d5cf288572434a487b055397b4cec51b/0f41b/2019-11-16-helmut-newton-foundation.jpg",
- "srcSet": "/static/d5cf288572434a487b055397b4cec51b/73124/2019-11-16-helmut-newton-foundation.jpg 75w,\n/static/d5cf288572434a487b055397b4cec51b/712e7/2019-11-16-helmut-newton-foundation.jpg 150w,\n/static/d5cf288572434a487b055397b4cec51b/0f41b/2019-11-16-helmut-newton-foundation.jpg 300w,\n/static/d5cf288572434a487b055397b4cec51b/270e8/2019-11-16-helmut-newton-foundation.jpg 450w,\n/static/d5cf288572434a487b055397b4cec51b/aa95b/2019-11-16-helmut-newton-foundation.jpg 600w,\n/static/d5cf288572434a487b055397b4cec51b/d3f44/2019-11-16-helmut-newton-foundation.jpg 3620w",
- "srcWebp": "/static/d5cf288572434a487b055397b4cec51b/40e2f/2019-11-16-helmut-newton-foundation.webp",
- "srcSetWebp": "/static/d5cf288572434a487b055397b4cec51b/37dc9/2019-11-16-helmut-newton-foundation.webp 75w,\n/static/d5cf288572434a487b055397b4cec51b/37a2a/2019-11-16-helmut-newton-foundation.webp 150w,\n/static/d5cf288572434a487b055397b4cec51b/40e2f/2019-11-16-helmut-newton-foundation.webp 300w,\n/static/d5cf288572434a487b055397b4cec51b/6a707/2019-11-16-helmut-newton-foundation.webp 450w,\n/static/d5cf288572434a487b055397b4cec51b/bf714/2019-11-16-helmut-newton-foundation.webp 600w,\n/static/d5cf288572434a487b055397b4cec51b/40f07/2019-11-16-helmut-newton-foundation.webp 3620w",
- "sizes": "(max-width: 300px) 100vw, 300px"
- }
- }
- }
- },
- "fields": {
- "slug": "/helmut-newton-foundation",
- "type": "photo"
- }
- }
- },
- {
- "node": {
- "id": "bbf028a7-0973-5d1e-8099-d6409936e73e",
- "frontmatter": {
- "title": "Országház III",
- "image": {
- "childImageSharp": {
- "original": {
- "src": "/static/2019-11-03-orszaghaz-iii-7c4460abda635fd1de2856343196a260.jpg"
- },
- "fluid": {
- "aspectRatio": 1,
- "src": "/static/7c4460abda635fd1de2856343196a260/0f41b/2019-11-03-orszaghaz-iii.jpg",
- "srcSet": "/static/7c4460abda635fd1de2856343196a260/73124/2019-11-03-orszaghaz-iii.jpg 75w,\n/static/7c4460abda635fd1de2856343196a260/712e7/2019-11-03-orszaghaz-iii.jpg 150w,\n/static/7c4460abda635fd1de2856343196a260/0f41b/2019-11-03-orszaghaz-iii.jpg 300w,\n/static/7c4460abda635fd1de2856343196a260/270e8/2019-11-03-orszaghaz-iii.jpg 450w,\n/static/7c4460abda635fd1de2856343196a260/aa95b/2019-11-03-orszaghaz-iii.jpg 600w,\n/static/7c4460abda635fd1de2856343196a260/9ce23/2019-11-03-orszaghaz-iii.jpg 3813w",
- "srcWebp": "/static/7c4460abda635fd1de2856343196a260/40e2f/2019-11-03-orszaghaz-iii.webp",
- "srcSetWebp": "/static/7c4460abda635fd1de2856343196a260/37dc9/2019-11-03-orszaghaz-iii.webp 75w,\n/static/7c4460abda635fd1de2856343196a260/37a2a/2019-11-03-orszaghaz-iii.webp 150w,\n/static/7c4460abda635fd1de2856343196a260/40e2f/2019-11-03-orszaghaz-iii.webp 300w,\n/static/7c4460abda635fd1de2856343196a260/6a707/2019-11-03-orszaghaz-iii.webp 450w,\n/static/7c4460abda635fd1de2856343196a260/bf714/2019-11-03-orszaghaz-iii.webp 600w,\n/static/7c4460abda635fd1de2856343196a260/e9670/2019-11-03-orszaghaz-iii.webp 3813w",
- "sizes": "(max-width: 300px) 100vw, 300px"
- }
- }
- }
- },
- "fields": {
- "slug": "/orszaghaz-iii",
- "type": "photo"
- }
- }
- },
- {
- "node": {
- "id": "6effe45d-0884-527c-9fba-6f7f567979fd",
- "frontmatter": {
- "title": "Országház II",
- "image": {
- "childImageSharp": {
- "original": {
- "src": "/static/2019-11-03-orszaghaz-ii-2dbcf257b4bdf625c24fede935d32425.jpg"
- },
- "fluid": {
- "aspectRatio": 1,
- "src": "/static/2dbcf257b4bdf625c24fede935d32425/0f41b/2019-11-03-orszaghaz-ii.jpg",
- "srcSet": "/static/2dbcf257b4bdf625c24fede935d32425/73124/2019-11-03-orszaghaz-ii.jpg 75w,\n/static/2dbcf257b4bdf625c24fede935d32425/712e7/2019-11-03-orszaghaz-ii.jpg 150w,\n/static/2dbcf257b4bdf625c24fede935d32425/0f41b/2019-11-03-orszaghaz-ii.jpg 300w,\n/static/2dbcf257b4bdf625c24fede935d32425/270e8/2019-11-03-orszaghaz-ii.jpg 450w,\n/static/2dbcf257b4bdf625c24fede935d32425/aa95b/2019-11-03-orszaghaz-ii.jpg 600w,\n/static/2dbcf257b4bdf625c24fede935d32425/d1a04/2019-11-03-orszaghaz-ii.jpg 3793w",
- "srcWebp": "/static/2dbcf257b4bdf625c24fede935d32425/40e2f/2019-11-03-orszaghaz-ii.webp",
- "srcSetWebp": "/static/2dbcf257b4bdf625c24fede935d32425/37dc9/2019-11-03-orszaghaz-ii.webp 75w,\n/static/2dbcf257b4bdf625c24fede935d32425/37a2a/2019-11-03-orszaghaz-ii.webp 150w,\n/static/2dbcf257b4bdf625c24fede935d32425/40e2f/2019-11-03-orszaghaz-ii.webp 300w,\n/static/2dbcf257b4bdf625c24fede935d32425/6a707/2019-11-03-orszaghaz-ii.webp 450w,\n/static/2dbcf257b4bdf625c24fede935d32425/bf714/2019-11-03-orszaghaz-ii.webp 600w,\n/static/2dbcf257b4bdf625c24fede935d32425/90141/2019-11-03-orszaghaz-ii.webp 3793w",
- "sizes": "(max-width: 300px) 100vw, 300px"
- }
- }
- }
- },
- "fields": {
- "slug": "/orszaghaz-ii",
- "type": "photo"
- }
- }
- },
- {
- "node": {
- "id": "c80697e5-681d-5fcc-9dab-c3a3821ff0b1",
- "frontmatter": {
- "title": "Országház I",
- "image": {
- "childImageSharp": {
- "original": {
- "src": "/static/2019-11-02-orszaghaz-i-36d87329aeeda296ae923606e5a4a785.jpg"
- },
- "fluid": {
- "aspectRatio": 1,
- "src": "/static/36d87329aeeda296ae923606e5a4a785/0f41b/2019-11-02-orszaghaz-i.jpg",
- "srcSet": "/static/36d87329aeeda296ae923606e5a4a785/73124/2019-11-02-orszaghaz-i.jpg 75w,\n/static/36d87329aeeda296ae923606e5a4a785/712e7/2019-11-02-orszaghaz-i.jpg 150w,\n/static/36d87329aeeda296ae923606e5a4a785/0f41b/2019-11-02-orszaghaz-i.jpg 300w,\n/static/36d87329aeeda296ae923606e5a4a785/270e8/2019-11-02-orszaghaz-i.jpg 450w,\n/static/36d87329aeeda296ae923606e5a4a785/aa95b/2019-11-02-orszaghaz-i.jpg 600w,\n/static/36d87329aeeda296ae923606e5a4a785/506d6/2019-11-02-orszaghaz-i.jpg 3708w",
- "srcWebp": "/static/36d87329aeeda296ae923606e5a4a785/40e2f/2019-11-02-orszaghaz-i.webp",
- "srcSetWebp": "/static/36d87329aeeda296ae923606e5a4a785/37dc9/2019-11-02-orszaghaz-i.webp 75w,\n/static/36d87329aeeda296ae923606e5a4a785/37a2a/2019-11-02-orszaghaz-i.webp 150w,\n/static/36d87329aeeda296ae923606e5a4a785/40e2f/2019-11-02-orszaghaz-i.webp 300w,\n/static/36d87329aeeda296ae923606e5a4a785/6a707/2019-11-02-orszaghaz-i.webp 450w,\n/static/36d87329aeeda296ae923606e5a4a785/bf714/2019-11-02-orszaghaz-i.webp 600w,\n/static/36d87329aeeda296ae923606e5a4a785/7fe6d/2019-11-02-orszaghaz-i.webp 3708w",
- "sizes": "(max-width: 300px) 100vw, 300px"
- }
- }
- }
- },
- "fields": {
- "slug": "/orszaghaz-i",
- "type": "photo"
- }
- }
- },
- {
- "node": {
- "id": "0549d0c5-1c41-5cf6-b640-6fbc6ab778fb",
- "frontmatter": {
- "title": "Foro di Cesare",
- "image": {
- "childImageSharp": {
- "original": {
- "src": "/static/2019-09-29-foro-di-cesare-00ba7297095683d32f490c1cf1a3ca2d.jpg"
- },
- "fluid": {
- "aspectRatio": 1,
- "src": "/static/00ba7297095683d32f490c1cf1a3ca2d/0f41b/2019-09-29-foro-di-cesare.jpg",
- "srcSet": "/static/00ba7297095683d32f490c1cf1a3ca2d/73124/2019-09-29-foro-di-cesare.jpg 75w,\n/static/00ba7297095683d32f490c1cf1a3ca2d/712e7/2019-09-29-foro-di-cesare.jpg 150w,\n/static/00ba7297095683d32f490c1cf1a3ca2d/0f41b/2019-09-29-foro-di-cesare.jpg 300w,\n/static/00ba7297095683d32f490c1cf1a3ca2d/270e8/2019-09-29-foro-di-cesare.jpg 450w,\n/static/00ba7297095683d32f490c1cf1a3ca2d/aa95b/2019-09-29-foro-di-cesare.jpg 600w,\n/static/00ba7297095683d32f490c1cf1a3ca2d/9dbe2/2019-09-29-foro-di-cesare.jpg 2868w",
- "srcWebp": "/static/00ba7297095683d32f490c1cf1a3ca2d/40e2f/2019-09-29-foro-di-cesare.webp",
- "srcSetWebp": "/static/00ba7297095683d32f490c1cf1a3ca2d/37dc9/2019-09-29-foro-di-cesare.webp 75w,\n/static/00ba7297095683d32f490c1cf1a3ca2d/37a2a/2019-09-29-foro-di-cesare.webp 150w,\n/static/00ba7297095683d32f490c1cf1a3ca2d/40e2f/2019-09-29-foro-di-cesare.webp 300w,\n/static/00ba7297095683d32f490c1cf1a3ca2d/6a707/2019-09-29-foro-di-cesare.webp 450w,\n/static/00ba7297095683d32f490c1cf1a3ca2d/bf714/2019-09-29-foro-di-cesare.webp 600w,\n/static/00ba7297095683d32f490c1cf1a3ca2d/d8318/2019-09-29-foro-di-cesare.webp 2868w",
- "sizes": "(max-width: 300px) 100vw, 300px"
- }
- }
- }
- },
- "fields": {
- "slug": "/foro-di-cesare",
- "type": "photo"
- }
- }
- },
- {
- "node": {
- "id": "fe3ed03e-3612-54cb-aceb-691938b4e3f3",
- "frontmatter": {
- "title": "Arco di Costantino",
- "image": {
- "childImageSharp": {
- "original": {
- "src": "/static/2019-09-29-arco-di-costantino-8f7392d32a0a7746cbb712e4e4a8e2ef.jpg"
- },
- "fluid": {
- "aspectRatio": 1,
- "src": "/static/8f7392d32a0a7746cbb712e4e4a8e2ef/0f41b/2019-09-29-arco-di-costantino.jpg",
- "srcSet": "/static/8f7392d32a0a7746cbb712e4e4a8e2ef/73124/2019-09-29-arco-di-costantino.jpg 75w,\n/static/8f7392d32a0a7746cbb712e4e4a8e2ef/712e7/2019-09-29-arco-di-costantino.jpg 150w,\n/static/8f7392d32a0a7746cbb712e4e4a8e2ef/0f41b/2019-09-29-arco-di-costantino.jpg 300w,\n/static/8f7392d32a0a7746cbb712e4e4a8e2ef/270e8/2019-09-29-arco-di-costantino.jpg 450w,\n/static/8f7392d32a0a7746cbb712e4e4a8e2ef/aa95b/2019-09-29-arco-di-costantino.jpg 600w,\n/static/8f7392d32a0a7746cbb712e4e4a8e2ef/30037/2019-09-29-arco-di-costantino.jpg 3024w",
- "srcWebp": "/static/8f7392d32a0a7746cbb712e4e4a8e2ef/40e2f/2019-09-29-arco-di-costantino.webp",
- "srcSetWebp": "/static/8f7392d32a0a7746cbb712e4e4a8e2ef/37dc9/2019-09-29-arco-di-costantino.webp 75w,\n/static/8f7392d32a0a7746cbb712e4e4a8e2ef/37a2a/2019-09-29-arco-di-costantino.webp 150w,\n/static/8f7392d32a0a7746cbb712e4e4a8e2ef/40e2f/2019-09-29-arco-di-costantino.webp 300w,\n/static/8f7392d32a0a7746cbb712e4e4a8e2ef/6a707/2019-09-29-arco-di-costantino.webp 450w,\n/static/8f7392d32a0a7746cbb712e4e4a8e2ef/bf714/2019-09-29-arco-di-costantino.webp 600w,\n/static/8f7392d32a0a7746cbb712e4e4a8e2ef/859d2/2019-09-29-arco-di-costantino.webp 3024w",
- "sizes": "(max-width: 300px) 100vw, 300px"
- }
- }
- }
- },
- "fields": {
- "slug": "/arco-di-costantino",
- "type": "photo"
- }
- }
- },
- {
- "node": {
- "id": "2cc51eaa-e6c0-521e-8cea-00cf68688161",
- "frontmatter": {
- "title": "Vatican Museums",
- "image": {
- "childImageSharp": {
- "original": {
- "src": "/static/2019-09-28-vatican-museums-9ffc375488247897643811d33d28edf9.jpg"
- },
- "fluid": {
- "aspectRatio": 1,
- "src": "/static/9ffc375488247897643811d33d28edf9/0f41b/2019-09-28-vatican-museums.jpg",
- "srcSet": "/static/9ffc375488247897643811d33d28edf9/73124/2019-09-28-vatican-museums.jpg 75w,\n/static/9ffc375488247897643811d33d28edf9/712e7/2019-09-28-vatican-museums.jpg 150w,\n/static/9ffc375488247897643811d33d28edf9/0f41b/2019-09-28-vatican-museums.jpg 300w,\n/static/9ffc375488247897643811d33d28edf9/270e8/2019-09-28-vatican-museums.jpg 450w,\n/static/9ffc375488247897643811d33d28edf9/aa95b/2019-09-28-vatican-museums.jpg 600w,\n/static/9ffc375488247897643811d33d28edf9/179de/2019-09-28-vatican-museums.jpg 2978w",
- "srcWebp": "/static/9ffc375488247897643811d33d28edf9/40e2f/2019-09-28-vatican-museums.webp",
- "srcSetWebp": "/static/9ffc375488247897643811d33d28edf9/37dc9/2019-09-28-vatican-museums.webp 75w,\n/static/9ffc375488247897643811d33d28edf9/37a2a/2019-09-28-vatican-museums.webp 150w,\n/static/9ffc375488247897643811d33d28edf9/40e2f/2019-09-28-vatican-museums.webp 300w,\n/static/9ffc375488247897643811d33d28edf9/6a707/2019-09-28-vatican-museums.webp 450w,\n/static/9ffc375488247897643811d33d28edf9/bf714/2019-09-28-vatican-museums.webp 600w,\n/static/9ffc375488247897643811d33d28edf9/79bab/2019-09-28-vatican-museums.webp 2978w",
- "sizes": "(max-width: 300px) 100vw, 300px"
- }
- }
- }
- },
- "fields": {
- "slug": "/vatican-museums",
- "type": "photo"
- }
- }
- },
- {
- "node": {
- "id": "9133fd25-1cb8-5660-be11-3532d4e18cae",
- "frontmatter": {
- "title": "German Chancellery II",
- "image": {
- "childImageSharp": {
- "original": {
- "src": "/static/2019-08-18-german-chancellery-ii-325bcee8ced12b150a2f3448ec1ae8cd.jpg"
- },
- "fluid": {
- "aspectRatio": 1,
- "src": "/static/325bcee8ced12b150a2f3448ec1ae8cd/0f41b/2019-08-18-german-chancellery-ii.jpg",
- "srcSet": "/static/325bcee8ced12b150a2f3448ec1ae8cd/73124/2019-08-18-german-chancellery-ii.jpg 75w,\n/static/325bcee8ced12b150a2f3448ec1ae8cd/712e7/2019-08-18-german-chancellery-ii.jpg 150w,\n/static/325bcee8ced12b150a2f3448ec1ae8cd/0f41b/2019-08-18-german-chancellery-ii.jpg 300w,\n/static/325bcee8ced12b150a2f3448ec1ae8cd/270e8/2019-08-18-german-chancellery-ii.jpg 450w,\n/static/325bcee8ced12b150a2f3448ec1ae8cd/aa95b/2019-08-18-german-chancellery-ii.jpg 600w,\n/static/325bcee8ced12b150a2f3448ec1ae8cd/0e49e/2019-08-18-german-chancellery-ii.jpg 4032w",
- "srcWebp": "/static/325bcee8ced12b150a2f3448ec1ae8cd/40e2f/2019-08-18-german-chancellery-ii.webp",
- "srcSetWebp": "/static/325bcee8ced12b150a2f3448ec1ae8cd/37dc9/2019-08-18-german-chancellery-ii.webp 75w,\n/static/325bcee8ced12b150a2f3448ec1ae8cd/37a2a/2019-08-18-german-chancellery-ii.webp 150w,\n/static/325bcee8ced12b150a2f3448ec1ae8cd/40e2f/2019-08-18-german-chancellery-ii.webp 300w,\n/static/325bcee8ced12b150a2f3448ec1ae8cd/6a707/2019-08-18-german-chancellery-ii.webp 450w,\n/static/325bcee8ced12b150a2f3448ec1ae8cd/bf714/2019-08-18-german-chancellery-ii.webp 600w,\n/static/325bcee8ced12b150a2f3448ec1ae8cd/b3fb4/2019-08-18-german-chancellery-ii.webp 4032w",
- "sizes": "(max-width: 300px) 100vw, 300px"
- }
- }
- }
- },
- "fields": {
- "slug": "/german-chancellery-ii",
- "type": "photo"
- }
- }
- }
- ]
- }
-}
diff --git a/.jest/__fixtures__/link.json b/.jest/__fixtures__/link.json
deleted file mode 100644
index e09f93c4..00000000
--- a/.jest/__fixtures__/link.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "post": {
- "html": "An awesome creative idea for a blog post about Web-Design from Joshua Clanton:
\n\nLeonardo Da Vinci was one of the greatest artists of the Renaissance, leaving behind a legacy that continues to inspire artists, scientists and others. Here are six things we can learn from him about web design.
\n ",
- "excerpt": "An awesome creative idea for a blog post about Web-Design from Joshua Clanton: Leonardo Da Vinci was one of the greatest artists of the…",
- "frontmatter": {
- "title": "6 Web Design Tips from Leonardo da Vinci",
- "image": null,
- "toc": null,
- "author": "Matthias Kretschmann",
- "updated": null,
- "tags": ["design"],
- "linkurl": "http://designpepper.com/blog/6-web-design-tips-from-leonardo-da-vinci",
- "style": null,
- "changelog": null
- },
- "fields": {
- "type": "link",
- "slug": "/6-web-design-tips-from-leonardo-da-vinci",
- "date": "2008-04-04T18:43:05.000Z",
- "githubLink": "https://github.com/kremalicious/blog/tree/main/content/posts/2008-04-04-6-web-design-tips-from-leonardo-da-vinci.md"
- },
- "rawMarkdownBody": "\nAn awesome creative idea for a blog post about Web-Design from Joshua Clanton:\n\n> Leonardo Da Vinci was one of the greatest artists of the Renaissance, leaving behind a legacy that continues to inspire artists, scientists and others. Here are six things we can learn from him about web design.\n",
- "tableOfContents": ""
- }
-}
diff --git a/.jest/__fixtures__/meta.json b/.jest/__fixtures__/meta.json
deleted file mode 100644
index dd3249ab..00000000
--- a/.jest/__fixtures__/meta.json
+++ /dev/null
@@ -1,43 +0,0 @@
-{
- "site": {
- "siteMetadata": {
- "siteTitle": "kremalicious",
- "siteTitleShort": "krlc",
- "siteDescription": "Blog of designer & developer Matthias Kretschmann",
- "siteUrl": "https://kremalicious.com",
- "author": {
- "name": "Matthias Kretschmann",
- "email": "m@kretschmann.io",
- "uri": "https://matthiaskretschmann.com",
- "twitter": "https://twitter.com/kremalicious",
- "mastodon": "https://mas.to/@krema",
- "github": "https://github.com/kremalicious",
- "facebook": "https://facebook.com/matthiaskretschmann",
- "bitcoin": "171qDmKEXm9YBgBLXyGjjPvopP5o9htQ1V",
- "ether": "0x339dbC44d39bf1961E385ed0Ae88FC6069b87Ea1"
- },
- "menu": [
- {
- "title": "Photos",
- "link": "/photos"
- },
- {
- "title": "Goodies",
- "link": "/tags/goodies"
- },
- {
- "title": "Tags",
- "link": "/tags"
- }
- ],
- "rss": "/feed.xml",
- "jsonfeed": "/feed.json",
- "itemsPerPage": 20,
- "repoContentPath": "https://github.com/kremalicious/blog/tree/main/content",
- "ad": {
- "title": "",
- "link": ""
- }
- }
- }
-}
diff --git a/.jest/__fixtures__/photos.json b/.jest/__fixtures__/photos.json
deleted file mode 100644
index c49c6715..00000000
--- a/.jest/__fixtures__/photos.json
+++ /dev/null
@@ -1,62 +0,0 @@
-{
- "allMarkdownRemark": {
- "edges": [
- {
- "node": {
- "id": "6effe45d-0884-527c-9fba-6f7f567979fd",
- "frontmatter": {
- "title": "Országház II",
- "image": {
- "childImageSharp": {
- "fluid": {
- "aspectRatio": 1,
- "src": "/static/2dbcf257b4bdf625c24fede935d32425/2423a/2019-11-03-orszaghaz-ii.jpg",
- "srcSet": "/static/2dbcf257b4bdf625c24fede935d32425/b7835/2019-11-03-orszaghaz-ii.jpg 100w,\n/static/2dbcf257b4bdf625c24fede935d32425/2de44/2019-11-03-orszaghaz-ii.jpg 200w,\n/static/2dbcf257b4bdf625c24fede935d32425/2423a/2019-11-03-orszaghaz-ii.jpg 400w,\n/static/2dbcf257b4bdf625c24fede935d32425/66039/2019-11-03-orszaghaz-ii.jpg 600w,\n/static/2dbcf257b4bdf625c24fede935d32425/ba820/2019-11-03-orszaghaz-ii.jpg 800w,\n/static/2dbcf257b4bdf625c24fede935d32425/d04d5/2019-11-03-orszaghaz-ii.jpg 3793w",
- "srcWebp": "/static/2dbcf257b4bdf625c24fede935d32425/33771/2019-11-03-orszaghaz-ii.webp",
- "srcSetWebp": "/static/2dbcf257b4bdf625c24fede935d32425/4f5da/2019-11-03-orszaghaz-ii.webp 100w,\n/static/2dbcf257b4bdf625c24fede935d32425/72345/2019-11-03-orszaghaz-ii.webp 200w,\n/static/2dbcf257b4bdf625c24fede935d32425/33771/2019-11-03-orszaghaz-ii.webp 400w,\n/static/2dbcf257b4bdf625c24fede935d32425/f12d8/2019-11-03-orszaghaz-ii.webp 600w,\n/static/2dbcf257b4bdf625c24fede935d32425/b975c/2019-11-03-orszaghaz-ii.webp 800w,\n/static/2dbcf257b4bdf625c24fede935d32425/f7332/2019-11-03-orszaghaz-ii.webp 3793w",
- "sizes": "(max-width: 400px) 100vw, 400px",
- "originalImg": "/static/2dbcf257b4bdf625c24fede935d32425/d04d5/2019-11-03-orszaghaz-ii.jpg",
- "originalName": "2019-11-03-orszaghaz-ii.jpg",
- "presentationWidth": 400,
- "presentationHeight": 300
- }
- }
- }
- },
- "fields": {
- "slug": "/orszaghaz-ii/",
- "type": "photo"
- }
- }
- },
- {
- "node": {
- "id": "c80697e5-681d-5fcc-9dab-c3a3821ff0b1",
- "frontmatter": {
- "title": "Országház I",
- "image": {
- "childImageSharp": {
- "fluid": {
- "aspectRatio": 1,
- "src": "/static/36d87329aeeda296ae923606e5a4a785/2423a/2019-11-02-orszaghaz-i.jpg",
- "srcSet": "/static/36d87329aeeda296ae923606e5a4a785/b7835/2019-11-02-orszaghaz-i.jpg 100w,\n/static/36d87329aeeda296ae923606e5a4a785/2de44/2019-11-02-orszaghaz-i.jpg 200w,\n/static/36d87329aeeda296ae923606e5a4a785/2423a/2019-11-02-orszaghaz-i.jpg 400w,\n/static/36d87329aeeda296ae923606e5a4a785/66039/2019-11-02-orszaghaz-i.jpg 600w,\n/static/36d87329aeeda296ae923606e5a4a785/ba820/2019-11-02-orszaghaz-i.jpg 800w,\n/static/36d87329aeeda296ae923606e5a4a785/c9c86/2019-11-02-orszaghaz-i.jpg 3708w",
- "srcWebp": "/static/36d87329aeeda296ae923606e5a4a785/33771/2019-11-02-orszaghaz-i.webp",
- "srcSetWebp": "/static/36d87329aeeda296ae923606e5a4a785/4f5da/2019-11-02-orszaghaz-i.webp 100w,\n/static/36d87329aeeda296ae923606e5a4a785/72345/2019-11-02-orszaghaz-i.webp 200w,\n/static/36d87329aeeda296ae923606e5a4a785/33771/2019-11-02-orszaghaz-i.webp 400w,\n/static/36d87329aeeda296ae923606e5a4a785/f12d8/2019-11-02-orszaghaz-i.webp 600w,\n/static/36d87329aeeda296ae923606e5a4a785/b975c/2019-11-02-orszaghaz-i.webp 800w,\n/static/36d87329aeeda296ae923606e5a4a785/262c1/2019-11-02-orszaghaz-i.webp 3708w",
- "sizes": "(max-width: 400px) 100vw, 400px",
- "originalImg": "/static/36d87329aeeda296ae923606e5a4a785/c9c86/2019-11-02-orszaghaz-i.jpg",
- "originalName": "2019-11-02-orszaghaz-i.jpg",
- "presentationWidth": 400,
- "presentationHeight": 297
- }
- }
- }
- },
- "fields": {
- "slug": "/orszaghaz-i/",
- "type": "photo"
- }
- }
- }
- ]
- }
-}
diff --git a/.jest/__fixtures__/post.json b/.jest/__fixtures__/post.json
deleted file mode 100644
index fdd44279..00000000
--- a/.jest/__fixtures__/post.json
+++ /dev/null
@@ -1,41 +0,0 @@
-{
- "post": {
- "html": "There are many reasons you might want to browse anonymously which can be accomplished by using Tor . The setup instructions on Tor’s website are quite scattered and outdated so here are some steps to setup Tor on macOS with a simple automated script at the end.
\nI’m using macOS Catalina (10.15) for the following instructions but it should work on almost any macOS version.
\n# Tor Browser \n\n \n \n \n \n Tor Browser \n
\nThe most simple way to surf anonymously with Tor is to just grab Tor Browser . It’s a modified version of Firefox Extended Support Release (ESR) with Tor and some extensions (Torbutton, TorLauncher, NoScript, and HTTPS-Everywhere) built right in. Upon start, Tor Browser automatically starts the required Tor background processes and routes traffic through the Tor network. That’s the way to go if you want the highest level of protection without much further configuration.
\nBut it’s based on an older version of Firefox and there might be more you want to do anonymously on your machine than just browsing the web, like accessing resources via the Terminal or any other app. Or just use the browser you’re used to.
\nFor this you need to have Tor installed on your system and additionally set specific proxy values in your network preferences after you’ve started Tor.
\n\n
Be aware that the instructions and the script mentioned below will not make whatever you do on the web anonymous. Much depends on your browsing habits, what apps you're using, and none of the methods below will offer the same level of protection than Tor Browser out of the box. You have been warned.
\n
\n# Install Tor \nContrary to the weirdly outdated install instructions on Tor’s website (hey, remember Macports?), installing Tor on macOS is super simple with Homebrew .
\nIn your Terminal execute:
\nbrew install tor
\nThen you can start it up by running:
\ntor
\nCongratulations, you now have Tor running on your system. But none of your network traffic is routed through it yet.
\nIn order for all your system traffic being routed through Tor you need to adjust your system’s network proxy settings which you can either do visually in the System Preferences or programmatically via macOS’s builtin networksetup
.
\n# Set network proxy settings via System Preferences \nYou can do this under System Preferences > Network by creating a specific Tor network location for it:
\n\nFrom Location dropdown at the top, select Edit Locations… \nCreate a new location by hitting the plus button and name it Tor . Hitting Done will select the new location which is now ready to be configured. \nGo to Advanced > Proxies and activate SOCKS Proxy and add those values: \nSOCKS proxy server : localhost
\nPort : 9050
\n \n\n \n \n \n \n Network Settings \n
\nAfter hitting OK & Apply at the initial network screen, you can easily switch to this newly created location from your menu bar under > Location whenever you start up Tor.
\nSwitching to the Tor location routes all network traffic on your system through Tor. Note that you have to repeat those steps for every other network interface if you use, say, Wi-Fi and Ethernet interchangeably.
\n# All in one go: start Tor & set network proxy settings automatically \nWhen you’re already in the Terminal to start up Tor, additionally setting the network settings involves a lot of fiddling around. Ain’t nobody got time for that.
\nThankfully macOS provides a way to programmatically set those proxy values via the networksetup
utility. I’ve found a nice script for this but running it opened multiple admin password prompts. So I extended it a bit to make it more user friendly.
\nIn a nutshell, this shell script asks you for your admin password upfront, starts up Tor, and sets all required proxy network settings automatically:
\n#!/usr/bin/env bash \n \n# 'Wi-Fi' or 'Ethernet' or 'Display Ethernet' \nINTERFACE=Wi-Fi \n \n# Ask for the administrator password upfront \nsudo -v \n \n# Keep-alive: update existing `sudo` time stamp until finished \nwhile true ; do sudo -n true ; sleep 60 ; kill -0 " $ $ " || exit ; done 2> /dev/null & \n \n# trap ctrl-c and call disable_proxy() \nfunction disable_proxy() { \n sudo networksetup -setsocksfirewallproxystate $ INTERFACE off \n echo "$( tput setaf 64 )" #green \n echo " SOCKS proxy disabled. " \n echo "$( tput sgr0 )" # color reset \n} \ntrap disable_proxy INT \n \n# Let's roll \nsudo networksetup -setsocksfirewallproxy $ INTERFACE 127.0.0.1 9050 off \nsudo networksetup -setsocksfirewallproxystate $ INTERFACE on \n \necho "$( tput setaf 64 )" # green \necho " SOCKS proxy 127.0.0.1:9050 enabled. " \necho "$( tput setaf 136 )" # orange \necho " Starting Tor... " \necho "$( tput sgr0 )" # color reset \n \ntor
\nSave this script under something like tor.sh
in one of your sourced bin
folders, make it executable with chmod + x
and use it as a replacement for the general tor
command. So you can just run
\ntor.sh
\nand Tor should run smoothly on your system without additional configuration:
\n\n \n \n \n \n Tor running in Terminal \n
\nVerify you’re indeed browsing over the Tor network by going to check.torproject.org .
\nWhen you’re done, just exit the script with ctrl + c and the network settings will be reverted to their previous configuration.
\n# Non-standard apps \nSome apps are just not good Mac citizens and use their own network settings, ignoring macOS system network proxy settings. E.g. older versions of Google Chrome were using their own custom network settings and therefore were not routing their web traffic through the proxy configured in System Preferences.
\nBut the most recent Chrome version automatically picks up macOS’s native proxy settings, as does the most recent version of Firefox.
\nAlways check your Tor connection with whatever app you’re using and if needed set the proxy preferences manually in the respective app with:
\n\nSOCKS proxy server : localhost
\nPort : 9050
\n \n",
- "excerpt": "There are many reasons you might want to browse anonymously which can be accomplished by using Tor. The setup instructions on Tor’s website…",
- "frontmatter": {
- "title": "Simple Tor setup on macOS",
- "image": {
- "childImageSharp": {
- "fluid": {
- "aspectRatio": 3.2,
- "src": "/static/c87c817b4191fb48e9ca4419c0c70bb8/3995d/teaser-tor.png",
- "srcSet": "/static/c87c817b4191fb48e9ca4419c0c70bb8/f0031/teaser-tor.png 200w,\n/static/c87c817b4191fb48e9ca4419c0c70bb8/cad89/teaser-tor.png 400w,\n/static/c87c817b4191fb48e9ca4419c0c70bb8/3995d/teaser-tor.png 800w,\n/static/c87c817b4191fb48e9ca4419c0c70bb8/32ce4/teaser-tor.png 1200w,\n/static/c87c817b4191fb48e9ca4419c0c70bb8/1ed7e/teaser-tor.png 1600w,\n/static/c87c817b4191fb48e9ca4419c0c70bb8/19dc3/teaser-tor.png 1920w",
- "srcWebp": "/static/c87c817b4191fb48e9ca4419c0c70bb8/240ac/teaser-tor.webp",
- "srcSetWebp": "/static/c87c817b4191fb48e9ca4419c0c70bb8/04c4c/teaser-tor.webp 200w,\n/static/c87c817b4191fb48e9ca4419c0c70bb8/ebea1/teaser-tor.webp 400w,\n/static/c87c817b4191fb48e9ca4419c0c70bb8/240ac/teaser-tor.webp 800w,\n/static/c87c817b4191fb48e9ca4419c0c70bb8/823f0/teaser-tor.webp 1200w,\n/static/c87c817b4191fb48e9ca4419c0c70bb8/9e49e/teaser-tor.webp 1600w,\n/static/c87c817b4191fb48e9ca4419c0c70bb8/68ae1/teaser-tor.webp 1920w",
- "sizes": "(max-width: 800px) 100vw, 800px",
- "originalImg": "/static/c87c817b4191fb48e9ca4419c0c70bb8/19dc3/teaser-tor.png",
- "originalName": "teaser-tor.png",
- "presentationWidth": 800,
- "presentationHeight": 250
- }
- },
- "fields": null
- },
- "toc": true,
- "author": "Matthias Kretschmann",
- "updated": "2019-11-07T22:52:46.000Z",
- "tags": ["tutorial", "tor", "macos", "goodies", "apple"],
- "linkurl": null,
- "style": null,
- "changelog": null
- },
- "fields": {
- "type": "post",
- "slug": "/simple-tor-setup-on-mac-os-x",
- "date": "2015-08-02T19:57:30.912Z",
- "githubLink": "https://github.com/kremalicious/blog/tree/main/content/posts/2015-08-02-simple-tor-setup-on-mac-os-x/index.md"
- },
- "rawMarkdownBody": "\nThere are many reasons you might want to browse anonymously which can be accomplished by using [Tor](https://www.torproject.org). The setup instructions on Tor's website are quite scattered and outdated so here are some steps to setup Tor on macOS with a simple automated script at the end.\n\nI'm using macOS Catalina (10.15) for the following instructions but it should work on almost any macOS version.\n\n## Tor Browser\n\n![Tor Browser](tor-browser.png)\n\nThe most simple way to surf anonymously with Tor is to just grab [Tor Browser](https://www.torproject.org/projects/torbrowser.html.en). It's a modified version of [Firefox Extended Support Release (ESR)](https://www.mozilla.org/en-US/firefox/organizations/) with Tor and some extensions (Torbutton, TorLauncher, NoScript, and HTTPS-Everywhere) built right in. Upon start, Tor Browser automatically starts the required Tor background processes and routes traffic through the Tor network. That's the way to go if you want the highest level of protection without much further configuration.\n\nBut it's based on an older version of Firefox and there might be more you want to do anonymously on your machine than just browsing the web, like accessing resources via the Terminal or any other app. Or just use the browser you're used to.\n\nFor this you need to have Tor installed on your system and additionally set specific proxy values in your network preferences after you've started Tor.\n\n\n
Be aware that the instructions and the script mentioned below will not make whatever you do on the web anonymous. Much depends on your browsing habits, what apps you're using, and none of the methods below will offer the same level of protection than Tor Browser out of the box. You have been warned.
\n
\n\n## Install Tor\n\nContrary to the weirdly outdated [install instructions on Tor's website](https://www.torproject.org/docs/tor-doc-osx.html.en) (hey, remember Macports?), installing Tor on macOS is super simple with [Homebrew](http://brew.sh).\n\nIn your Terminal execute:\n\n```bash\nbrew install tor\n```\n\nThen you can start it up by running:\n\n```bash\ntor\n```\n\nCongratulations, you now have Tor running on your system. But none of your network traffic is routed through it yet.\n\nIn order for all your system traffic being routed through Tor you need to adjust your system's network proxy settings which you can either do visually in the System Preferences or programmatically via macOS's builtin `networksetup`.\n\n## Set network proxy settings via System Preferences\n\nYou can do this under _System Preferences > Network_ by creating a specific Tor network location for it:\n\n1. From Location dropdown at the top, select _Edit Locations..._\n2. Create a new location by hitting the plus button and name it _Tor_. Hitting Done will select the new location which is now ready to be configured.\n3. Go to _Advanced > Proxies_ and activate _SOCKS Proxy_ and add those values:\n\n- _SOCKS proxy server_: `localhost`\n- _Port_: `9050`\n\n![Network Settings](tor-osx-proxy.png)\n\nAfter hitting _OK_ & _Apply_ at the initial network screen, you can easily switch to this newly created location from your menu bar under _ > Location_ whenever you start up Tor.\n\nSwitching to the Tor location routes all network traffic on your system through Tor. Note that you have to repeat those steps for every other network interface if you use, say, Wi-Fi and Ethernet interchangeably.\n\n## All in one go: start Tor & set network proxy settings automatically\n\nWhen you're already in the Terminal to start up Tor, additionally setting the network settings involves a lot of fiddling around. Ain't nobody got time for that.\n\nThankfully macOS provides a way to programmatically set those proxy values via the `networksetup` utility. I've found a [nice script](http://leonid.shevtsov.me/en/an-easy-way-to-use-tor-on-os-x) for this but running it opened multiple admin password prompts. So I extended it a bit to make it more user friendly.\n\nIn a nutshell, this shell script asks you for your admin password upfront, starts up Tor, and sets all required proxy network settings automatically:\n\n```bash\n#!/usr/bin/env bash\n\n# 'Wi-Fi' or 'Ethernet' or 'Display Ethernet'\nINTERFACE=Wi-Fi\n\n# Ask for the administrator password upfront\nsudo -v\n\n# Keep-alive: update existing `sudo` time stamp until finished\nwhile true; do sudo -n true; sleep 60; kill -0 \"$$\" || exit; done 2>/dev/null &\n\n# trap ctrl-c and call disable_proxy()\nfunction disable_proxy() {\n sudo networksetup -setsocksfirewallproxystate $INTERFACE off\n echo \"$(tput setaf 64)\" #green\n echo \"SOCKS proxy disabled.\"\n echo \"$(tput sgr0)\" # color reset\n}\ntrap disable_proxy INT\n\n# Let's roll\nsudo networksetup -setsocksfirewallproxy $INTERFACE 127.0.0.1 9050 off\nsudo networksetup -setsocksfirewallproxystate $INTERFACE on\n\necho \"$(tput setaf 64)\" # green\necho \"SOCKS proxy 127.0.0.1:9050 enabled.\"\necho \"$(tput setaf 136)\" # orange\necho \"Starting Tor...\"\necho \"$(tput sgr0)\" # color reset\n\ntor\n```\n\nSave this script under something like `tor.sh` in one of your sourced `bin` folders, make it executable with `chmod + x` and use it as a replacement for the general `tor` command. So you can just run\n\n```bash\ntor.sh\n```\n\nand Tor should run smoothly on your system without additional configuration:\n\n![Tor running in Terminal](tor-osx-terminal.png)\n\nVerify you're indeed browsing over the Tor network by going to [check.torproject.org](https://check.torproject.org).\n\nWhen you're done, just exit the script with ctrl + c and the network settings will be reverted to their previous configuration.\n\n## Non-standard apps\n\nSome apps are just not good Mac citizens and use their own network settings, ignoring macOS system network proxy settings. E.g. older versions of Google Chrome were using their own custom network settings and therefore were not routing their web traffic through the proxy configured in System Preferences.\n\nBut the most recent Chrome version automatically picks up macOS's native proxy settings, as does the most recent version of Firefox.\n\nAlways [check](https://check.torproject.org) your Tor connection with whatever app you're using and if needed set the proxy preferences manually in the respective app with:\n\n- _SOCKS proxy server_: `localhost`\n- _Port_: `9050`\n",
- "tableOfContents": ""
- }
-}
diff --git a/.jest/__fixtures__/postWithMore.json b/.jest/__fixtures__/postWithMore.json
deleted file mode 100644
index 2d3255d4..00000000
--- a/.jest/__fixtures__/postWithMore.json
+++ /dev/null
@@ -1,41 +0,0 @@
-{
- "post": {
- "html": "There are many reasons you might want to browse anonymously which can be accomplished by using Tor . The setup instructions on Tor’s website are quite scattered and outdated so here are some steps to setup Tor on macOS with a simple automated script at the end.
\nI’m using macOS Catalina (10.15) for the following instructions but it should work on almost any macOS version.
\n# Tor Browser \n\n \n \n \n \n Tor Browser \n
\nThe most simple way to surf anonymously with Tor is to just grab Tor Browser . It’s a modified version of Firefox Extended Support Release (ESR) with Tor and some extensions (Torbutton, TorLauncher, NoScript, and HTTPS-Everywhere) built right in. Upon start, Tor Browser automatically starts the required Tor background processes and routes traffic through the Tor network. That’s the way to go if you want the highest level of protection without much further configuration.
\nBut it’s based on an older version of Firefox and there might be more you want to do anonymously on your machine than just browsing the web, like accessing resources via the Terminal or any other app. Or just use the browser you’re used to.
\nFor this you need to have Tor installed on your system and additionally set specific proxy values in your network preferences after you’ve started Tor.
\n\n
Be aware that the instructions and the script mentioned below will not make whatever you do on the web anonymous. Much depends on your browsing habits, what apps you're using, and none of the methods below will offer the same level of protection than Tor Browser out of the box. You have been warned.
\n
\n# Install Tor \nContrary to the weirdly outdated install instructions on Tor’s website (hey, remember Macports?), installing Tor on macOS is super simple with Homebrew .
\nIn your Terminal execute:
\nbrew install tor
\nThen you can start it up by running:
\ntor
\nCongratulations, you now have Tor running on your system. But none of your network traffic is routed through it yet.
\nIn order for all your system traffic being routed through Tor you need to adjust your system’s network proxy settings which you can either do visually in the System Preferences or programmatically via macOS’s builtin networksetup
.
\n# Set network proxy settings via System Preferences \nYou can do this under System Preferences > Network by creating a specific Tor network location for it:
\n\nFrom Location dropdown at the top, select Edit Locations… \nCreate a new location by hitting the plus button and name it Tor . Hitting Done will select the new location which is now ready to be configured. \nGo to Advanced > Proxies and activate SOCKS Proxy and add those values: \nSOCKS proxy server : localhost
\nPort : 9050
\n \n\n \n \n \n \n Network Settings \n
\nAfter hitting OK & Apply at the initial network screen, you can easily switch to this newly created location from your menu bar under > Location whenever you start up Tor.
\nSwitching to the Tor location routes all network traffic on your system through Tor. Note that you have to repeat those steps for every other network interface if you use, say, Wi-Fi and Ethernet interchangeably.
\n# All in one go: start Tor & set network proxy settings automatically \nWhen you’re already in the Terminal to start up Tor, additionally setting the network settings involves a lot of fiddling around. Ain’t nobody got time for that.
\nThankfully macOS provides a way to programmatically set those proxy values via the networksetup
utility. I’ve found a nice script for this but running it opened multiple admin password prompts. So I extended it a bit to make it more user friendly.
\nIn a nutshell, this shell script asks you for your admin password upfront, starts up Tor, and sets all required proxy network settings automatically:
\n#!/usr/bin/env bash \n \n# 'Wi-Fi' or 'Ethernet' or 'Display Ethernet' \nINTERFACE=Wi-Fi \n \n# Ask for the administrator password upfront \nsudo -v \n \n# Keep-alive: update existing `sudo` time stamp until finished \nwhile true ; do sudo -n true ; sleep 60 ; kill -0 " $ $ " || exit ; done 2> /dev/null & \n \n# trap ctrl-c and call disable_proxy() \nfunction disable_proxy() { \n sudo networksetup -setsocksfirewallproxystate $ INTERFACE off \n echo "$( tput setaf 64 )" #green \n echo " SOCKS proxy disabled. " \n echo "$( tput sgr0 )" # color reset \n} \ntrap disable_proxy INT \n \n# Let's roll \nsudo networksetup -setsocksfirewallproxy $ INTERFACE 127.0.0.1 9050 off \nsudo networksetup -setsocksfirewallproxystate $ INTERFACE on \n \necho "$( tput setaf 64 )" # green \necho " SOCKS proxy 127.0.0.1:9050 enabled. " \necho "$( tput setaf 136 )" # orange \necho " Starting Tor... " \necho "$( tput sgr0 )" # color reset \n \ntor
\nSave this script under something like tor.sh
in one of your sourced bin
folders, make it executable with chmod + x
and use it as a replacement for the general tor
command. So you can just run
\ntor.sh
\nand Tor should run smoothly on your system without additional configuration:
\n\n \n \n \n \n Tor running in Terminal \n
\nVerify you’re indeed browsing over the Tor network by going to check.torproject.org .
\nWhen you’re done, just exit the script with ctrl + c and the network settings will be reverted to their previous configuration.
\n# Non-standard apps \nSome apps are just not good Mac citizens and use their own network settings, ignoring macOS system network proxy settings. E.g. older versions of Google Chrome were using their own custom network settings and therefore were not routing their web traffic through the proxy configured in System Preferences.
\nBut the most recent Chrome version automatically picks up macOS’s native proxy settings, as does the most recent version of Firefox.
\nAlways check your Tor connection with whatever app you’re using and if needed set the proxy preferences manually in the respective app with:
\n\nSOCKS proxy server : localhost
\nPort : 9050
\n \n",
- "excerpt": "There are many reasons you might want to browse anonymously which can be accomplished by using Tor. The setup instructions on Tor’s website…",
- "frontmatter": {
- "type": "post",
- "title": "Simple Tor setup on macOS",
- "image": {
- "childImageSharp": {
- "fluid": {
- "aspectRatio": 3.2,
- "src": "/static/c87c817b4191fb48e9ca4419c0c70bb8/3995d/teaser-tor.png",
- "srcSet": "/static/c87c817b4191fb48e9ca4419c0c70bb8/f0031/teaser-tor.png 200w,\n/static/c87c817b4191fb48e9ca4419c0c70bb8/cad89/teaser-tor.png 400w,\n/static/c87c817b4191fb48e9ca4419c0c70bb8/3995d/teaser-tor.png 800w,\n/static/c87c817b4191fb48e9ca4419c0c70bb8/32ce4/teaser-tor.png 1200w,\n/static/c87c817b4191fb48e9ca4419c0c70bb8/1ed7e/teaser-tor.png 1600w,\n/static/c87c817b4191fb48e9ca4419c0c70bb8/19dc3/teaser-tor.png 1920w",
- "srcWebp": "/static/c87c817b4191fb48e9ca4419c0c70bb8/240ac/teaser-tor.webp",
- "srcSetWebp": "/static/c87c817b4191fb48e9ca4419c0c70bb8/04c4c/teaser-tor.webp 200w,\n/static/c87c817b4191fb48e9ca4419c0c70bb8/ebea1/teaser-tor.webp 400w,\n/static/c87c817b4191fb48e9ca4419c0c70bb8/240ac/teaser-tor.webp 800w,\n/static/c87c817b4191fb48e9ca4419c0c70bb8/823f0/teaser-tor.webp 1200w,\n/static/c87c817b4191fb48e9ca4419c0c70bb8/9e49e/teaser-tor.webp 1600w,\n/static/c87c817b4191fb48e9ca4419c0c70bb8/68ae1/teaser-tor.webp 1920w",
- "sizes": "(max-width: 800px) 100vw, 800px",
- "originalImg": "/static/c87c817b4191fb48e9ca4419c0c70bb8/19dc3/teaser-tor.png",
- "originalName": "teaser-tor.png",
- "presentationWidth": 800,
- "presentationHeight": 250
- }
- },
- "fields": null
- },
- "toc": true,
- "author": "Matthias Kretschmann",
- "updated": "2019-11-07T22:52:46.000Z",
- "tags": ["tutorial", "tor", "macos", "goodies", "apple"],
- "linkurl": null,
- "style": null,
- "changelog": null
- },
- "fields": {
- "slug": "/simple-tor-setup-on-mac-os-x",
- "date": "2015-08-02T19:57:30.912Z",
- "githubLink": "https://github.com/kremalicious/blog/tree/main/content/posts/2015-08-02-simple-tor-setup-on-mac-os-x/index.md"
- },
- "rawMarkdownBody": "\nThere are many reasons you might want to browse anonymously which can be accomplished by using [Tor](https://www.torproject.org). The setup instructions on Tor's website are quite scattered and outdated so here are some steps to setup Tor on macOS with a simple automated script at the end.\n\nI'm using macOS Catalina (10.15) for the following instructions but it should work on almost any macOS version.\n\n## Tor Browser\n\n![Tor Browser](tor-browser.png)\n\nThe most simple way to surf anonymously with Tor is to just grab [Tor Browser](https://www.torproject.org/projects/torbrowser.html.en). It's a modified version of [Firefox Extended Support Release (ESR)](https://www.mozilla.org/en-US/firefox/organizations/) with Tor and some extensions (Torbutton, TorLauncher, NoScript, and HTTPS-Everywhere) built right in. Upon start, Tor Browser automatically starts the required Tor background processes and routes traffic through the Tor network. That's the way to go if you want the highest level of protection without much further configuration.\n\nBut it's based on an older version of Firefox and there might be more you want to do anonymously on your machine than just browsing the web, like accessing resources via the Terminal or any other app. Or just use the browser you're used to.\n\nFor this you need to have Tor installed on your system and additionally set specific proxy values in your network preferences after you've started Tor.\n\n\n
Be aware that the instructions and the script mentioned below will not make whatever you do on the web anonymous. Much depends on your browsing habits, what apps you're using, and none of the methods below will offer the same level of protection than Tor Browser out of the box. You have been warned.
\n
\n\n## Install Tor\n\nContrary to the weirdly outdated [install instructions on Tor's website](https://www.torproject.org/docs/tor-doc-osx.html.en) (hey, remember Macports?), installing Tor on macOS is super simple with [Homebrew](http://brew.sh).\n\nIn your Terminal execute:\n\n```bash\nbrew install tor\n```\n\nThen you can start it up by running:\n\n```bash\ntor\n```\n\nCongratulations, you now have Tor running on your system. But none of your network traffic is routed through it yet.\n\nIn order for all your system traffic being routed through Tor you need to adjust your system's network proxy settings which you can either do visually in the System Preferences or programmatically via macOS's builtin `networksetup`.\n\n## Set network proxy settings via System Preferences\n\nYou can do this under _System Preferences > Network_ by creating a specific Tor network location for it:\n\n1. From Location dropdown at the top, select _Edit Locations..._\n2. Create a new location by hitting the plus button and name it _Tor_. Hitting Done will select the new location which is now ready to be configured.\n3. Go to _Advanced > Proxies_ and activate _SOCKS Proxy_ and add those values:\n\n- _SOCKS proxy server_: `localhost`\n- _Port_: `9050`\n\n![Network Settings](tor-osx-proxy.png)\n\nAfter hitting _OK_ & _Apply_ at the initial network screen, you can easily switch to this newly created location from your menu bar under _ > Location_ whenever you start up Tor.\n\nSwitching to the Tor location routes all network traffic on your system through Tor. Note that you have to repeat those steps for every other network interface if you use, say, Wi-Fi and Ethernet interchangeably.\n\n## All in one go: start Tor & set network proxy settings automatically\n\nWhen you're already in the Terminal to start up Tor, additionally setting the network settings involves a lot of fiddling around. Ain't nobody got time for that.\n\nThankfully macOS provides a way to programmatically set those proxy values via the `networksetup` utility. I've found a [nice script](http://leonid.shevtsov.me/en/an-easy-way-to-use-tor-on-os-x) for this but running it opened multiple admin password prompts. So I extended it a bit to make it more user friendly.\n\nIn a nutshell, this shell script asks you for your admin password upfront, starts up Tor, and sets all required proxy network settings automatically:\n\n```bash\n#!/usr/bin/env bash\n\n# 'Wi-Fi' or 'Ethernet' or 'Display Ethernet'\nINTERFACE=Wi-Fi\n\n# Ask for the administrator password upfront\nsudo -v\n\n# Keep-alive: update existing `sudo` time stamp until finished\nwhile true; do sudo -n true; sleep 60; kill -0 \"$$\" || exit; done 2>/dev/null &\n\n# trap ctrl-c and call disable_proxy()\nfunction disable_proxy() {\n sudo networksetup -setsocksfirewallproxystate $INTERFACE off\n echo \"$(tput setaf 64)\" #green\n echo \"SOCKS proxy disabled.\"\n echo \"$(tput sgr0)\" # color reset\n}\ntrap disable_proxy INT\n\n# Let's roll\nsudo networksetup -setsocksfirewallproxy $INTERFACE 127.0.0.1 9050 off\nsudo networksetup -setsocksfirewallproxystate $INTERFACE on\n\necho \"$(tput setaf 64)\" # green\necho \"SOCKS proxy 127.0.0.1:9050 enabled.\"\necho \"$(tput setaf 136)\" # orange\necho \"Starting Tor...\"\necho \"$(tput sgr0)\" # color reset\n\ntor\n```\n\nSave this script under something like `tor.sh` in one of your sourced `bin` folders, make it executable with `chmod + x` and use it as a replacement for the general `tor` command. So you can just run\n\n```bash\ntor.sh\n```\n\nand Tor should run smoothly on your system without additional configuration:\n\n![Tor running in Terminal](tor-osx-terminal.png)\n\nVerify you're indeed browsing over the Tor network by going to [check.torproject.org](https://check.torproject.org).\n\nWhen you're done, just exit the script with ctrl + c and the network settings will be reverted to their previous configuration.\n\n## Non-standard apps\n\nSome apps are just not good Mac citizens and use their own network settings, ignoring macOS system network proxy settings. E.g. older versions of Google Chrome were using their own custom network settings and therefore were not routing their web traffic through the proxy configured in System Preferences.\n\nBut the most recent Chrome version automatically picks up macOS's native proxy settings, as does the most recent version of Firefox.\n\nAlways [check](https://check.torproject.org) your Tor connection with whatever app you're using and if needed set the proxy preferences manually in the respective app with:\n\n- _SOCKS proxy server_: `localhost`\n- _Port_: `9050`\n",
- "tableOfContents": ""
- }
-}
diff --git a/.jest/__fixtures__/posts.json b/.jest/__fixtures__/posts.json
deleted file mode 100644
index 4dbc52d5..00000000
--- a/.jest/__fixtures__/posts.json
+++ /dev/null
@@ -1,356 +0,0 @@
-{
- "allMarkdownRemark": {
- "edges": [
- {
- "node": {
- "id": "6effe45d-0884-527c-9fba-6f7f567979fd",
- "fileAbsolutePath": "/Users/m/Code/blog/content/photos/2019-11-03-orszaghaz-ii.md",
- "html": "Inside the Hungarian Parliament Building in Budapest, Hungary.
",
- "excerpt": "Inside the Hungarian Parliament Building in Budapest, Hungary.",
- "frontmatter": {
- "title": "Országház II",
-
- "linkurl": null,
- "tags": null,
- "image": {
- "childImageSharp": {
- "original": {
- "src": "/static/2019-11-03-orszaghaz-ii-2dbcf257b4bdf625c24fede935d32425.jpg"
- },
- "fluid": {
- "aspectRatio": 2.3255813953488373,
- "src": "/static/2dbcf257b4bdf625c24fede935d32425/ef7a0/2019-11-03-orszaghaz-ii.jpg",
- "srcSet": "/static/2dbcf257b4bdf625c24fede935d32425/23780/2019-11-03-orszaghaz-ii.jpg 100w,\n/static/2dbcf257b4bdf625c24fede935d32425/02ed9/2019-11-03-orszaghaz-ii.jpg 200w,\n/static/2dbcf257b4bdf625c24fede935d32425/ef7a0/2019-11-03-orszaghaz-ii.jpg 400w,\n/static/2dbcf257b4bdf625c24fede935d32425/ac974/2019-11-03-orszaghaz-ii.jpg 600w,\n/static/2dbcf257b4bdf625c24fede935d32425/12c26/2019-11-03-orszaghaz-ii.jpg 800w,\n/static/2dbcf257b4bdf625c24fede935d32425/24914/2019-11-03-orszaghaz-ii.jpg 3793w",
- "srcWebp": "/static/2dbcf257b4bdf625c24fede935d32425/a93fc/2019-11-03-orszaghaz-ii.webp",
- "srcSetWebp": "/static/2dbcf257b4bdf625c24fede935d32425/b0720/2019-11-03-orszaghaz-ii.webp 100w,\n/static/2dbcf257b4bdf625c24fede935d32425/f6188/2019-11-03-orszaghaz-ii.webp 200w,\n/static/2dbcf257b4bdf625c24fede935d32425/a93fc/2019-11-03-orszaghaz-ii.webp 400w,\n/static/2dbcf257b4bdf625c24fede935d32425/7c0bb/2019-11-03-orszaghaz-ii.webp 600w,\n/static/2dbcf257b4bdf625c24fede935d32425/d1e4e/2019-11-03-orszaghaz-ii.webp 800w,\n/static/2dbcf257b4bdf625c24fede935d32425/0a478/2019-11-03-orszaghaz-ii.webp 3793w",
- "sizes": "(max-width: 400px) 100vw, 400px",
- "originalImg": "/static/2dbcf257b4bdf625c24fede935d32425/24914/2019-11-03-orszaghaz-ii.jpg",
- "originalName": "2019-11-03-orszaghaz-ii.jpg",
- "presentationWidth": 400,
- "presentationHeight": 300
- }
- }
- }
- },
- "fields": {
- "slug": "/orszaghaz-ii/",
- "date": "November 03, 2019",
- "type": "photo"
- }
- }
- },
- {
- "node": {
- "id": "c80697e5-681d-5fcc-9dab-c3a3821ff0b1",
- "fileAbsolutePath": "/Users/m/Code/blog/content/photos/2019-11-02-orszaghaz-i.md",
- "html": "The Hungarian Parliament Building seen from across the Danube in Budapest, Hungary.
",
- "excerpt": "The Hungarian Parliament Building seen from across the Danube in Budapest, Hungary.",
- "frontmatter": {
- "title": "Országház I",
- "linkurl": null,
- "tags": null,
- "featured": true,
- "image": {
- "childImageSharp": {
- "original": {
- "src": "/static/2019-11-02-orszaghaz-i-36d87329aeeda296ae923606e5a4a785.jpg"
- },
- "fluid": {
- "aspectRatio": 2.3255813953488373,
- "src": "/static/36d87329aeeda296ae923606e5a4a785/ef7a0/2019-11-02-orszaghaz-i.jpg",
- "srcSet": "/static/36d87329aeeda296ae923606e5a4a785/23780/2019-11-02-orszaghaz-i.jpg 100w,\n/static/36d87329aeeda296ae923606e5a4a785/02ed9/2019-11-02-orszaghaz-i.jpg 200w,\n/static/36d87329aeeda296ae923606e5a4a785/ef7a0/2019-11-02-orszaghaz-i.jpg 400w,\n/static/36d87329aeeda296ae923606e5a4a785/ac974/2019-11-02-orszaghaz-i.jpg 600w,\n/static/36d87329aeeda296ae923606e5a4a785/12c26/2019-11-02-orszaghaz-i.jpg 800w,\n/static/36d87329aeeda296ae923606e5a4a785/b9e8b/2019-11-02-orszaghaz-i.jpg 3708w",
- "srcWebp": "/static/36d87329aeeda296ae923606e5a4a785/a93fc/2019-11-02-orszaghaz-i.webp",
- "srcSetWebp": "/static/36d87329aeeda296ae923606e5a4a785/b0720/2019-11-02-orszaghaz-i.webp 100w,\n/static/36d87329aeeda296ae923606e5a4a785/f6188/2019-11-02-orszaghaz-i.webp 200w,\n/static/36d87329aeeda296ae923606e5a4a785/a93fc/2019-11-02-orszaghaz-i.webp 400w,\n/static/36d87329aeeda296ae923606e5a4a785/7c0bb/2019-11-02-orszaghaz-i.webp 600w,\n/static/36d87329aeeda296ae923606e5a4a785/d1e4e/2019-11-02-orszaghaz-i.webp 800w,\n/static/36d87329aeeda296ae923606e5a4a785/730f8/2019-11-02-orszaghaz-i.webp 3708w",
- "sizes": "(max-width: 400px) 100vw, 400px",
- "originalImg": "/static/36d87329aeeda296ae923606e5a4a785/b9e8b/2019-11-02-orszaghaz-i.jpg",
- "originalName": "2019-11-02-orszaghaz-i.jpg",
- "presentationWidth": 400,
- "presentationHeight": 297
- }
- }
- }
- },
- "fields": {
- "slug": "/orszaghaz-i/",
- "date": "November 02, 2019",
- "type": "photo"
- }
- }
- },
- {
- "node": {
- "id": "2e2c9611-be7b-5bc0-a72b-6c63e2072b5a",
- "fileAbsolutePath": "/Users/m/Code/blog/content/posts/2019-10-24-ocean-protocol-and-ipfs-sitting-in-the-merkle-tree/index.md",
- "html": "IPFS is now integrated into the Ocean Protocol stack, allowing you to take advantage of decentralized asset file hosting.
\n \n\nThis article was originally posted on Medium in the Ocean Protocol blog .
\n \n \n# ✨ Going Decentralized \nWith Ocean Protocol, you can use centralized storage services like S3, Azure Storage, or your own On-Premise storage to store and retrieve your asset files through Osmosis drivers in Brizo .
\nBut storing asset files in a centralized service poses multiple problems:
\n\none entity controls the data \none entity is legally responsible for all stored data \ncreates a single point of failure \nif service goes offline, asset files can’t be consumed \nopening up possibilities of censorship by the entity running the service, or the service itself \nif files are moved to another location within the same service, existing URLs break \n \nInitially created to store and efficiently move scientific data sets, the InterPlanetary File System (IPFS) solves all those issues with its goal of transforming the vastly centralized web into a distributed peer-to-peer network.
\nFiles are distributed among multiple nodes, eliminating the single point of failure, legal, and censorship issues. By using content-based instead of location-based addressing of files, URLs won’t break if files are moved.
\nSo we defined OEP-15 to make the ipfs://
protocol a first-class citizen in the Ocean Protocol stack, allowing you to store asset files on IPFS, and use their native IPFS URLs during the publish process.
\n\nIn short, every component in the Ocean Protocol stack now supports publishing and consuming of asset files stored in IPFS which includes support for native IPFS URLs, referencing files with their Content Identifiers (CIDs).
\n# ⛲️ IPFS in Commons Marketplace \nEvery file stored on IPFS is public by default, so it made perfect sense using this in our Commons Marketplace first. We went through multiple prototypes to end up with our final setup.
\nDuring the publish flow you will find an extended Files section for adding a file from an existing URL, and for adding a local file from your device to IPFS.
\n\n \n \n \n \n \n \n Add existing IPFS asset under commons.oceanprotocol.com/publish \n
\nThe existing URL field now supports ipfs://
in addition to http(s)://
so if you have an existing asset on IPFS, you can add it here and everything works as before. With the exception that the asset files will be registered with this native IPFS URL.
\nThe new IPFS drop zone provides a convenient way to add and register unpublished asset files.
\n\n \n \n \n \n \n \n Add to IPFS from a local file under commons.oceanprotocol.com/publish \n
\nThat drop zone allows you to add a file from your local machine and add it to IPFS during the asset publish flow in a snap:
\n
\n# The Tech Details \nFirst, opening up the drop zone area will ping the IPFS node & gateway to check connectivity. Dropping or selecting a file from your device onto that area does a bunch of things in the background:
\n\nWill add that file to an IPFS node with js-ipfs-http-client , and pin the file so it stays on our node during garbage collection. We wrapped the HTTP client into our own custom React Hook . \nThe file will be wrapped into a directory to preserve the original file name, so we end up with a URL like ipfs://QmX5LRpEVocfks9FNDnRoK2imf2fy9mPpP4wfgaDVXWfYD/video.mp4
. \nThe returned CID is used to ping that file over an IPFS gateway to make it globally available. \nThe IPFS gateway URL is passed to our Commons Server file checker, extracting some file metadata and checking for availability of the file. \nThe native IPFS url is passed to the list of asset files, and it will show up in the file list. \nUpon final asset publishing, the native IPFS URL is stored encrypted in the asset DID Descriptor Object (DDO) as defined in OEP-8 . \n \nAll these steps are required because of how files in IPFS are distributed among its nodes. When adding a file to a node (note how it’s not called “uploading”) it is only available on that node. Requesting that file from another node will start the distribution process of the file from the initial IPFS node, making it globally available.
\nAlso the consume flow required some changes. Whenever an asset file stored on IPFS is requested to be downloaded, multiple things related to IPFS are happening in Brizo with its shiny new osmosis-ipfs-driver :
\n\nThe file URLs are decrypted upon successful fulfilling of all conditions. \nThe native ipfs://
url is mapped to its https://
gateway URL. \nFile is downloaded from that gateway URL. \nIn case a direct file URL was used instead of being folder-wrapped (e.g. ipfs://QmPQNaNxHkasNiJBFZht2k3zCEBkvEu1aE5VNGZ8QiT8Nj
), the proper file ending will be added automatically at the end of the download process, based on detected MIME type. \n \n# 🏄♀️ Ocean Protocol’s Public IPFS Node & Gateway \nWhile developing this feature, it became clear we need to run our own IPFS node & gateway to have better control over the whole experience. And donating a node instead of taking away bandwidth from the main IPFS gateway (gateway.ipfs.io) felt like the right thing to do to make Juan happy.
\nSo we created ipfs.oceanprotocol.com , run by us (that is, legally speaking, BigchainDB GmbH).
\n\n \n \n \n \n \n \n Frontpage for ipfs.oceanprotocol.com \n
\nIt is setup to be a public gateway, and provide some access to its node HTTP API for everyone. This means you can use it to address any content in the IPFS network, like:
\n\nAt the same time all node API functionality required by Commons is open to the world, so those endpoints can be used by anyone to add files to IPFS:
\n\n/api/v0/add
\n/api/v0/version
\n/api/v0/id
\n \nAs a start, this is a simple single node but we plan to gradually upgrade ipfs.oceanprotocol.com to a full IPFS Cluster of multiple nodes for best data availability.
\n\n# ⛴ Next Possible Iterations \nBeside upgrading to an IPFS Cluster there are many ways the process will be improved over time. At the moment the drop zone in Commons only supports single file uploads, so a quick win improvement would be to allow dropping multiple files at once. Likewise, the drop zone using js-ipfs-http-client comes with some bugs when trying to upload larger files.
\nTo make the process of adding files to the IPFS node less dependent on the client browser, it could be moved into a background task in the Commons Server. This should also give more control and feedback for the process of distributing a file from the initial node to other nodes.
\nAnd finally, further work may be done to store files encrypted on IPFS and implement a way to decrypt them in an Ocean Protocol network.
\n \n# Learn more about the Commons Marketplace \n\n \n\nThis article was originally posted on Medium in the Ocean Protocol blog .
\n \n ",
- "excerpt": "IPFS is now integrated into the Ocean Protocol stack, allowing you to take advantage of decentralized asset file hosting. This article was originally posted on Medium in the Ocean Protocol blog. ✨ Going Decentralized With Ocean Protocol, you can use…",
- "frontmatter": {
- "title": "Ocean Protocol and IPFS, Sitting In The Merkle Tree",
-
- "linkurl": null,
- "tags": ["oceanprotocol", "blockchain", "design", "ipfs", "web3"],
- "image": {
- "childImageSharp": {
- "original": {
- "src": "/static/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser-f724bdaae38e81ec90b01ec6b8412cc8.png"
- },
- "fluid": {
- "aspectRatio": 2.3255813953488373,
- "src": "/static/f724bdaae38e81ec90b01ec6b8412cc8/687f7/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png",
- "srcSet": "/static/f724bdaae38e81ec90b01ec6b8412cc8/90a60/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png 100w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/c23b6/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png 200w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/687f7/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png 400w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/a9eb1/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png 600w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/9a629/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png 800w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/5956c/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png 2000w",
- "srcWebp": "/static/f724bdaae38e81ec90b01ec6b8412cc8/a93fc/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.webp",
- "srcSetWebp": "/static/f724bdaae38e81ec90b01ec6b8412cc8/b0720/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.webp 100w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/f6188/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.webp 200w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/a93fc/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.webp 400w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/7c0bb/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.webp 600w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/d1e4e/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.webp 800w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/eb8cf/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.webp 2000w",
- "sizes": "(max-width: 400px) 100vw, 400px",
- "originalImg": "/static/f724bdaae38e81ec90b01ec6b8412cc8/5956c/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png",
- "originalName": "ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png",
- "presentationWidth": 400,
- "presentationHeight": 140
- }
- }
- }
- },
- "fields": {
- "slug": "/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree",
- "date": "October 24, 2019",
- "type": "post"
- }
- }
- }
- ]
- },
- "allArticles": {
- "edges": [
- {
- "node": {
- "id": "6effe45d-0884-527c-9fba-6f7f567979fd",
- "fileAbsolutePath": "/Users/m/Code/blog/content/photos/2019-11-03-orszaghaz-ii.md",
- "html": "Inside the Hungarian Parliament Building in Budapest, Hungary.
",
- "excerpt": "Inside the Hungarian Parliament Building in Budapest, Hungary.",
- "frontmatter": {
- "title": "Országház II",
-
- "linkurl": null,
- "tags": null,
- "image": {
- "childImageSharp": {
- "original": {
- "src": "/static/2019-11-03-orszaghaz-ii-2dbcf257b4bdf625c24fede935d32425.jpg"
- },
- "fluid": {
- "aspectRatio": 2.3255813953488373,
- "src": "/static/2dbcf257b4bdf625c24fede935d32425/ef7a0/2019-11-03-orszaghaz-ii.jpg",
- "srcSet": "/static/2dbcf257b4bdf625c24fede935d32425/23780/2019-11-03-orszaghaz-ii.jpg 100w,\n/static/2dbcf257b4bdf625c24fede935d32425/02ed9/2019-11-03-orszaghaz-ii.jpg 200w,\n/static/2dbcf257b4bdf625c24fede935d32425/ef7a0/2019-11-03-orszaghaz-ii.jpg 400w,\n/static/2dbcf257b4bdf625c24fede935d32425/ac974/2019-11-03-orszaghaz-ii.jpg 600w,\n/static/2dbcf257b4bdf625c24fede935d32425/12c26/2019-11-03-orszaghaz-ii.jpg 800w,\n/static/2dbcf257b4bdf625c24fede935d32425/24914/2019-11-03-orszaghaz-ii.jpg 3793w",
- "srcWebp": "/static/2dbcf257b4bdf625c24fede935d32425/a93fc/2019-11-03-orszaghaz-ii.webp",
- "srcSetWebp": "/static/2dbcf257b4bdf625c24fede935d32425/b0720/2019-11-03-orszaghaz-ii.webp 100w,\n/static/2dbcf257b4bdf625c24fede935d32425/f6188/2019-11-03-orszaghaz-ii.webp 200w,\n/static/2dbcf257b4bdf625c24fede935d32425/a93fc/2019-11-03-orszaghaz-ii.webp 400w,\n/static/2dbcf257b4bdf625c24fede935d32425/7c0bb/2019-11-03-orszaghaz-ii.webp 600w,\n/static/2dbcf257b4bdf625c24fede935d32425/d1e4e/2019-11-03-orszaghaz-ii.webp 800w,\n/static/2dbcf257b4bdf625c24fede935d32425/0a478/2019-11-03-orszaghaz-ii.webp 3793w",
- "sizes": "(max-width: 400px) 100vw, 400px",
- "originalImg": "/static/2dbcf257b4bdf625c24fede935d32425/24914/2019-11-03-orszaghaz-ii.jpg",
- "originalName": "2019-11-03-orszaghaz-ii.jpg",
- "presentationWidth": 400,
- "presentationHeight": 300
- }
- }
- }
- },
- "fields": {
- "slug": "/orszaghaz-ii/",
- "date": "November 03, 2019",
- "type": "photo"
- }
- }
- },
- {
- "node": {
- "id": "c80697e5-681d-5fcc-9dab-c3a3821ff0b1",
- "fileAbsolutePath": "/Users/m/Code/blog/content/photos/2019-11-02-orszaghaz-i.md",
- "html": "The Hungarian Parliament Building seen from across the Danube in Budapest, Hungary.
",
- "excerpt": "The Hungarian Parliament Building seen from across the Danube in Budapest, Hungary.",
- "frontmatter": {
- "title": "Országház I",
- "linkurl": null,
- "tags": null,
- "featured": true,
- "image": {
- "childImageSharp": {
- "original": {
- "src": "/static/2019-11-02-orszaghaz-i-36d87329aeeda296ae923606e5a4a785.jpg"
- },
- "fluid": {
- "aspectRatio": 2.3255813953488373,
- "src": "/static/36d87329aeeda296ae923606e5a4a785/ef7a0/2019-11-02-orszaghaz-i.jpg",
- "srcSet": "/static/36d87329aeeda296ae923606e5a4a785/23780/2019-11-02-orszaghaz-i.jpg 100w,\n/static/36d87329aeeda296ae923606e5a4a785/02ed9/2019-11-02-orszaghaz-i.jpg 200w,\n/static/36d87329aeeda296ae923606e5a4a785/ef7a0/2019-11-02-orszaghaz-i.jpg 400w,\n/static/36d87329aeeda296ae923606e5a4a785/ac974/2019-11-02-orszaghaz-i.jpg 600w,\n/static/36d87329aeeda296ae923606e5a4a785/12c26/2019-11-02-orszaghaz-i.jpg 800w,\n/static/36d87329aeeda296ae923606e5a4a785/b9e8b/2019-11-02-orszaghaz-i.jpg 3708w",
- "srcWebp": "/static/36d87329aeeda296ae923606e5a4a785/a93fc/2019-11-02-orszaghaz-i.webp",
- "srcSetWebp": "/static/36d87329aeeda296ae923606e5a4a785/b0720/2019-11-02-orszaghaz-i.webp 100w,\n/static/36d87329aeeda296ae923606e5a4a785/f6188/2019-11-02-orszaghaz-i.webp 200w,\n/static/36d87329aeeda296ae923606e5a4a785/a93fc/2019-11-02-orszaghaz-i.webp 400w,\n/static/36d87329aeeda296ae923606e5a4a785/7c0bb/2019-11-02-orszaghaz-i.webp 600w,\n/static/36d87329aeeda296ae923606e5a4a785/d1e4e/2019-11-02-orszaghaz-i.webp 800w,\n/static/36d87329aeeda296ae923606e5a4a785/730f8/2019-11-02-orszaghaz-i.webp 3708w",
- "sizes": "(max-width: 400px) 100vw, 400px",
- "originalImg": "/static/36d87329aeeda296ae923606e5a4a785/b9e8b/2019-11-02-orszaghaz-i.jpg",
- "originalName": "2019-11-02-orszaghaz-i.jpg",
- "presentationWidth": 400,
- "presentationHeight": 297
- }
- }
- }
- },
- "fields": {
- "slug": "/orszaghaz-i/",
- "date": "November 02, 2019",
- "type": "photo"
- }
- }
- },
- {
- "node": {
- "id": "2e2c9611-be7b-5bc0-a72b-6c63e2072b5a",
- "fileAbsolutePath": "/Users/m/Code/blog/content/posts/2019-10-24-ocean-protocol-and-ipfs-sitting-in-the-merkle-tree/index.md",
- "html": "IPFS is now integrated into the Ocean Protocol stack, allowing you to take advantage of decentralized asset file hosting.
\n \n\nThis article was originally posted on Medium in the Ocean Protocol blog .
\n \n \n# ✨ Going Decentralized \nWith Ocean Protocol, you can use centralized storage services like S3, Azure Storage, or your own On-Premise storage to store and retrieve your asset files through Osmosis drivers in Brizo .
\nBut storing asset files in a centralized service poses multiple problems:
\n\none entity controls the data \none entity is legally responsible for all stored data \ncreates a single point of failure \nif service goes offline, asset files can’t be consumed \nopening up possibilities of censorship by the entity running the service, or the service itself \nif files are moved to another location within the same service, existing URLs break \n \nInitially created to store and efficiently move scientific data sets, the InterPlanetary File System (IPFS) solves all those issues with its goal of transforming the vastly centralized web into a distributed peer-to-peer network.
\nFiles are distributed among multiple nodes, eliminating the single point of failure, legal, and censorship issues. By using content-based instead of location-based addressing of files, URLs won’t break if files are moved.
\nSo we defined OEP-15 to make the ipfs://
protocol a first-class citizen in the Ocean Protocol stack, allowing you to store asset files on IPFS, and use their native IPFS URLs during the publish process.
\n\nIn short, every component in the Ocean Protocol stack now supports publishing and consuming of asset files stored in IPFS which includes support for native IPFS URLs, referencing files with their Content Identifiers (CIDs).
\n# ⛲️ IPFS in Commons Marketplace \nEvery file stored on IPFS is public by default, so it made perfect sense using this in our Commons Marketplace first. We went through multiple prototypes to end up with our final setup.
\nDuring the publish flow you will find an extended Files section for adding a file from an existing URL, and for adding a local file from your device to IPFS.
\n\n \n \n \n \n \n \n Add existing IPFS asset under commons.oceanprotocol.com/publish \n
\nThe existing URL field now supports ipfs://
in addition to http(s)://
so if you have an existing asset on IPFS, you can add it here and everything works as before. With the exception that the asset files will be registered with this native IPFS URL.
\nThe new IPFS drop zone provides a convenient way to add and register unpublished asset files.
\n\n \n \n \n \n \n \n Add to IPFS from a local file under commons.oceanprotocol.com/publish \n
\nThat drop zone allows you to add a file from your local machine and add it to IPFS during the asset publish flow in a snap:
\n
\n# The Tech Details \nFirst, opening up the drop zone area will ping the IPFS node & gateway to check connectivity. Dropping or selecting a file from your device onto that area does a bunch of things in the background:
\n\nWill add that file to an IPFS node with js-ipfs-http-client , and pin the file so it stays on our node during garbage collection. We wrapped the HTTP client into our own custom React Hook . \nThe file will be wrapped into a directory to preserve the original file name, so we end up with a URL like ipfs://QmX5LRpEVocfks9FNDnRoK2imf2fy9mPpP4wfgaDVXWfYD/video.mp4
. \nThe returned CID is used to ping that file over an IPFS gateway to make it globally available. \nThe IPFS gateway URL is passed to our Commons Server file checker, extracting some file metadata and checking for availability of the file. \nThe native IPFS url is passed to the list of asset files, and it will show up in the file list. \nUpon final asset publishing, the native IPFS URL is stored encrypted in the asset DID Descriptor Object (DDO) as defined in OEP-8 . \n \nAll these steps are required because of how files in IPFS are distributed among its nodes. When adding a file to a node (note how it’s not called “uploading”) it is only available on that node. Requesting that file from another node will start the distribution process of the file from the initial IPFS node, making it globally available.
\nAlso the consume flow required some changes. Whenever an asset file stored on IPFS is requested to be downloaded, multiple things related to IPFS are happening in Brizo with its shiny new osmosis-ipfs-driver :
\n\nThe file URLs are decrypted upon successful fulfilling of all conditions. \nThe native ipfs://
url is mapped to its https://
gateway URL. \nFile is downloaded from that gateway URL. \nIn case a direct file URL was used instead of being folder-wrapped (e.g. ipfs://QmPQNaNxHkasNiJBFZht2k3zCEBkvEu1aE5VNGZ8QiT8Nj
), the proper file ending will be added automatically at the end of the download process, based on detected MIME type. \n \n# 🏄♀️ Ocean Protocol’s Public IPFS Node & Gateway \nWhile developing this feature, it became clear we need to run our own IPFS node & gateway to have better control over the whole experience. And donating a node instead of taking away bandwidth from the main IPFS gateway (gateway.ipfs.io) felt like the right thing to do to make Juan happy.
\nSo we created ipfs.oceanprotocol.com , run by us (that is, legally speaking, BigchainDB GmbH).
\n\n \n \n \n \n \n \n Frontpage for ipfs.oceanprotocol.com \n
\nIt is setup to be a public gateway, and provide some access to its node HTTP API for everyone. This means you can use it to address any content in the IPFS network, like:
\n\nAt the same time all node API functionality required by Commons is open to the world, so those endpoints can be used by anyone to add files to IPFS:
\n\n/api/v0/add
\n/api/v0/version
\n/api/v0/id
\n \nAs a start, this is a simple single node but we plan to gradually upgrade ipfs.oceanprotocol.com to a full IPFS Cluster of multiple nodes for best data availability.
\n\n# ⛴ Next Possible Iterations \nBeside upgrading to an IPFS Cluster there are many ways the process will be improved over time. At the moment the drop zone in Commons only supports single file uploads, so a quick win improvement would be to allow dropping multiple files at once. Likewise, the drop zone using js-ipfs-http-client comes with some bugs when trying to upload larger files.
\nTo make the process of adding files to the IPFS node less dependent on the client browser, it could be moved into a background task in the Commons Server. This should also give more control and feedback for the process of distributing a file from the initial node to other nodes.
\nAnd finally, further work may be done to store files encrypted on IPFS and implement a way to decrypt them in an Ocean Protocol network.
\n \n# Learn more about the Commons Marketplace \n\n \n\nThis article was originally posted on Medium in the Ocean Protocol blog .
\n \n ",
- "excerpt": "IPFS is now integrated into the Ocean Protocol stack, allowing you to take advantage of decentralized asset file hosting. This article was originally posted on Medium in the Ocean Protocol blog. ✨ Going Decentralized With Ocean Protocol, you can use…",
- "frontmatter": {
- "title": "Ocean Protocol and IPFS, Sitting In The Merkle Tree",
-
- "linkurl": null,
- "tags": ["oceanprotocol", "blockchain", "design", "ipfs", "web3"],
- "image": {
- "childImageSharp": {
- "original": {
- "src": "/static/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser-f724bdaae38e81ec90b01ec6b8412cc8.png"
- },
- "fluid": {
- "aspectRatio": 2.3255813953488373,
- "src": "/static/f724bdaae38e81ec90b01ec6b8412cc8/687f7/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png",
- "srcSet": "/static/f724bdaae38e81ec90b01ec6b8412cc8/90a60/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png 100w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/c23b6/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png 200w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/687f7/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png 400w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/a9eb1/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png 600w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/9a629/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png 800w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/5956c/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png 2000w",
- "srcWebp": "/static/f724bdaae38e81ec90b01ec6b8412cc8/a93fc/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.webp",
- "srcSetWebp": "/static/f724bdaae38e81ec90b01ec6b8412cc8/b0720/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.webp 100w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/f6188/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.webp 200w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/a93fc/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.webp 400w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/7c0bb/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.webp 600w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/d1e4e/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.webp 800w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/eb8cf/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.webp 2000w",
- "sizes": "(max-width: 400px) 100vw, 400px",
- "originalImg": "/static/f724bdaae38e81ec90b01ec6b8412cc8/5956c/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png",
- "originalName": "ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png",
- "presentationWidth": 400,
- "presentationHeight": 140
- }
- }
- }
- },
- "fields": {
- "slug": "/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree",
- "date": "October 24, 2019",
- "type": "post"
- }
- }
- }
- ]
- },
- "allPhotos": {
- "edges": [
- {
- "node": {
- "id": "6effe45d-0884-527c-9fba-6f7f567979fd",
- "fileAbsolutePath": "/Users/m/Code/blog/content/photos/2019-11-03-orszaghaz-ii.md",
- "html": "Inside the Hungarian Parliament Building in Budapest, Hungary.
",
- "excerpt": "Inside the Hungarian Parliament Building in Budapest, Hungary.",
- "frontmatter": {
- "title": "Országház II",
-
- "linkurl": null,
- "tags": null,
- "image": {
- "childImageSharp": {
- "original": {
- "src": "/static/2019-11-03-orszaghaz-ii-2dbcf257b4bdf625c24fede935d32425.jpg"
- },
- "fluid": {
- "aspectRatio": 2.3255813953488373,
- "src": "/static/2dbcf257b4bdf625c24fede935d32425/ef7a0/2019-11-03-orszaghaz-ii.jpg",
- "srcSet": "/static/2dbcf257b4bdf625c24fede935d32425/23780/2019-11-03-orszaghaz-ii.jpg 100w,\n/static/2dbcf257b4bdf625c24fede935d32425/02ed9/2019-11-03-orszaghaz-ii.jpg 200w,\n/static/2dbcf257b4bdf625c24fede935d32425/ef7a0/2019-11-03-orszaghaz-ii.jpg 400w,\n/static/2dbcf257b4bdf625c24fede935d32425/ac974/2019-11-03-orszaghaz-ii.jpg 600w,\n/static/2dbcf257b4bdf625c24fede935d32425/12c26/2019-11-03-orszaghaz-ii.jpg 800w,\n/static/2dbcf257b4bdf625c24fede935d32425/24914/2019-11-03-orszaghaz-ii.jpg 3793w",
- "srcWebp": "/static/2dbcf257b4bdf625c24fede935d32425/a93fc/2019-11-03-orszaghaz-ii.webp",
- "srcSetWebp": "/static/2dbcf257b4bdf625c24fede935d32425/b0720/2019-11-03-orszaghaz-ii.webp 100w,\n/static/2dbcf257b4bdf625c24fede935d32425/f6188/2019-11-03-orszaghaz-ii.webp 200w,\n/static/2dbcf257b4bdf625c24fede935d32425/a93fc/2019-11-03-orszaghaz-ii.webp 400w,\n/static/2dbcf257b4bdf625c24fede935d32425/7c0bb/2019-11-03-orszaghaz-ii.webp 600w,\n/static/2dbcf257b4bdf625c24fede935d32425/d1e4e/2019-11-03-orszaghaz-ii.webp 800w,\n/static/2dbcf257b4bdf625c24fede935d32425/0a478/2019-11-03-orszaghaz-ii.webp 3793w",
- "sizes": "(max-width: 400px) 100vw, 400px",
- "originalImg": "/static/2dbcf257b4bdf625c24fede935d32425/24914/2019-11-03-orszaghaz-ii.jpg",
- "originalName": "2019-11-03-orszaghaz-ii.jpg",
- "presentationWidth": 400,
- "presentationHeight": 300
- }
- }
- }
- },
- "fields": {
- "slug": "/orszaghaz-ii/",
- "date": "November 03, 2019",
- "type": "photo"
- }
- }
- },
- {
- "node": {
- "id": "c80697e5-681d-5fcc-9dab-c3a3821ff0b1",
- "fileAbsolutePath": "/Users/m/Code/blog/content/photos/2019-11-02-orszaghaz-i.md",
- "html": "The Hungarian Parliament Building seen from across the Danube in Budapest, Hungary.
",
- "excerpt": "The Hungarian Parliament Building seen from across the Danube in Budapest, Hungary.",
- "frontmatter": {
- "title": "Országház I",
- "linkurl": null,
- "tags": null,
- "featured": true,
- "image": {
- "childImageSharp": {
- "original": {
- "src": "/static/2019-11-02-orszaghaz-i-36d87329aeeda296ae923606e5a4a785.jpg"
- },
- "fluid": {
- "aspectRatio": 2.3255813953488373,
- "src": "/static/36d87329aeeda296ae923606e5a4a785/ef7a0/2019-11-02-orszaghaz-i.jpg",
- "srcSet": "/static/36d87329aeeda296ae923606e5a4a785/23780/2019-11-02-orszaghaz-i.jpg 100w,\n/static/36d87329aeeda296ae923606e5a4a785/02ed9/2019-11-02-orszaghaz-i.jpg 200w,\n/static/36d87329aeeda296ae923606e5a4a785/ef7a0/2019-11-02-orszaghaz-i.jpg 400w,\n/static/36d87329aeeda296ae923606e5a4a785/ac974/2019-11-02-orszaghaz-i.jpg 600w,\n/static/36d87329aeeda296ae923606e5a4a785/12c26/2019-11-02-orszaghaz-i.jpg 800w,\n/static/36d87329aeeda296ae923606e5a4a785/b9e8b/2019-11-02-orszaghaz-i.jpg 3708w",
- "srcWebp": "/static/36d87329aeeda296ae923606e5a4a785/a93fc/2019-11-02-orszaghaz-i.webp",
- "srcSetWebp": "/static/36d87329aeeda296ae923606e5a4a785/b0720/2019-11-02-orszaghaz-i.webp 100w,\n/static/36d87329aeeda296ae923606e5a4a785/f6188/2019-11-02-orszaghaz-i.webp 200w,\n/static/36d87329aeeda296ae923606e5a4a785/a93fc/2019-11-02-orszaghaz-i.webp 400w,\n/static/36d87329aeeda296ae923606e5a4a785/7c0bb/2019-11-02-orszaghaz-i.webp 600w,\n/static/36d87329aeeda296ae923606e5a4a785/d1e4e/2019-11-02-orszaghaz-i.webp 800w,\n/static/36d87329aeeda296ae923606e5a4a785/730f8/2019-11-02-orszaghaz-i.webp 3708w",
- "sizes": "(max-width: 400px) 100vw, 400px",
- "originalImg": "/static/36d87329aeeda296ae923606e5a4a785/b9e8b/2019-11-02-orszaghaz-i.jpg",
- "originalName": "2019-11-02-orszaghaz-i.jpg",
- "presentationWidth": 400,
- "presentationHeight": 297
- }
- }
- }
- },
- "fields": {
- "slug": "/orszaghaz-i/",
- "date": "November 02, 2019",
- "type": "photo"
- }
- }
- },
- {
- "node": {
- "id": "2e2c9611-be7b-5bc0-a72b-6c63e2072b5a",
- "fileAbsolutePath": "/Users/m/Code/blog/content/posts/2019-10-24-ocean-protocol-and-ipfs-sitting-in-the-merkle-tree/index.md",
- "html": "IPFS is now integrated into the Ocean Protocol stack, allowing you to take advantage of decentralized asset file hosting.
\n \n\nThis article was originally posted on Medium in the Ocean Protocol blog .
\n \n \n# ✨ Going Decentralized \nWith Ocean Protocol, you can use centralized storage services like S3, Azure Storage, or your own On-Premise storage to store and retrieve your asset files through Osmosis drivers in Brizo .
\nBut storing asset files in a centralized service poses multiple problems:
\n\none entity controls the data \none entity is legally responsible for all stored data \ncreates a single point of failure \nif service goes offline, asset files can’t be consumed \nopening up possibilities of censorship by the entity running the service, or the service itself \nif files are moved to another location within the same service, existing URLs break \n \nInitially created to store and efficiently move scientific data sets, the InterPlanetary File System (IPFS) solves all those issues with its goal of transforming the vastly centralized web into a distributed peer-to-peer network.
\nFiles are distributed among multiple nodes, eliminating the single point of failure, legal, and censorship issues. By using content-based instead of location-based addressing of files, URLs won’t break if files are moved.
\nSo we defined OEP-15 to make the ipfs://
protocol a first-class citizen in the Ocean Protocol stack, allowing you to store asset files on IPFS, and use their native IPFS URLs during the publish process.
\n\nIn short, every component in the Ocean Protocol stack now supports publishing and consuming of asset files stored in IPFS which includes support for native IPFS URLs, referencing files with their Content Identifiers (CIDs).
\n# ⛲️ IPFS in Commons Marketplace \nEvery file stored on IPFS is public by default, so it made perfect sense using this in our Commons Marketplace first. We went through multiple prototypes to end up with our final setup.
\nDuring the publish flow you will find an extended Files section for adding a file from an existing URL, and for adding a local file from your device to IPFS.
\n\n \n \n \n \n \n \n Add existing IPFS asset under commons.oceanprotocol.com/publish \n
\nThe existing URL field now supports ipfs://
in addition to http(s)://
so if you have an existing asset on IPFS, you can add it here and everything works as before. With the exception that the asset files will be registered with this native IPFS URL.
\nThe new IPFS drop zone provides a convenient way to add and register unpublished asset files.
\n\n \n \n \n \n \n \n Add to IPFS from a local file under commons.oceanprotocol.com/publish \n
\nThat drop zone allows you to add a file from your local machine and add it to IPFS during the asset publish flow in a snap:
\n
\n# The Tech Details \nFirst, opening up the drop zone area will ping the IPFS node & gateway to check connectivity. Dropping or selecting a file from your device onto that area does a bunch of things in the background:
\n\nWill add that file to an IPFS node with js-ipfs-http-client , and pin the file so it stays on our node during garbage collection. We wrapped the HTTP client into our own custom React Hook . \nThe file will be wrapped into a directory to preserve the original file name, so we end up with a URL like ipfs://QmX5LRpEVocfks9FNDnRoK2imf2fy9mPpP4wfgaDVXWfYD/video.mp4
. \nThe returned CID is used to ping that file over an IPFS gateway to make it globally available. \nThe IPFS gateway URL is passed to our Commons Server file checker, extracting some file metadata and checking for availability of the file. \nThe native IPFS url is passed to the list of asset files, and it will show up in the file list. \nUpon final asset publishing, the native IPFS URL is stored encrypted in the asset DID Descriptor Object (DDO) as defined in OEP-8 . \n \nAll these steps are required because of how files in IPFS are distributed among its nodes. When adding a file to a node (note how it’s not called “uploading”) it is only available on that node. Requesting that file from another node will start the distribution process of the file from the initial IPFS node, making it globally available.
\nAlso the consume flow required some changes. Whenever an asset file stored on IPFS is requested to be downloaded, multiple things related to IPFS are happening in Brizo with its shiny new osmosis-ipfs-driver :
\n\nThe file URLs are decrypted upon successful fulfilling of all conditions. \nThe native ipfs://
url is mapped to its https://
gateway URL. \nFile is downloaded from that gateway URL. \nIn case a direct file URL was used instead of being folder-wrapped (e.g. ipfs://QmPQNaNxHkasNiJBFZht2k3zCEBkvEu1aE5VNGZ8QiT8Nj
), the proper file ending will be added automatically at the end of the download process, based on detected MIME type. \n \n# 🏄♀️ Ocean Protocol’s Public IPFS Node & Gateway \nWhile developing this feature, it became clear we need to run our own IPFS node & gateway to have better control over the whole experience. And donating a node instead of taking away bandwidth from the main IPFS gateway (gateway.ipfs.io) felt like the right thing to do to make Juan happy.
\nSo we created ipfs.oceanprotocol.com , run by us (that is, legally speaking, BigchainDB GmbH).
\n\n \n \n \n \n \n \n Frontpage for ipfs.oceanprotocol.com \n
\nIt is setup to be a public gateway, and provide some access to its node HTTP API for everyone. This means you can use it to address any content in the IPFS network, like:
\n\nAt the same time all node API functionality required by Commons is open to the world, so those endpoints can be used by anyone to add files to IPFS:
\n\n/api/v0/add
\n/api/v0/version
\n/api/v0/id
\n \nAs a start, this is a simple single node but we plan to gradually upgrade ipfs.oceanprotocol.com to a full IPFS Cluster of multiple nodes for best data availability.
\n\n# ⛴ Next Possible Iterations \nBeside upgrading to an IPFS Cluster there are many ways the process will be improved over time. At the moment the drop zone in Commons only supports single file uploads, so a quick win improvement would be to allow dropping multiple files at once. Likewise, the drop zone using js-ipfs-http-client comes with some bugs when trying to upload larger files.
\nTo make the process of adding files to the IPFS node less dependent on the client browser, it could be moved into a background task in the Commons Server. This should also give more control and feedback for the process of distributing a file from the initial node to other nodes.
\nAnd finally, further work may be done to store files encrypted on IPFS and implement a way to decrypt them in an Ocean Protocol network.
\n \n# Learn more about the Commons Marketplace \n\n \n\nThis article was originally posted on Medium in the Ocean Protocol blog .
\n \n ",
- "excerpt": "IPFS is now integrated into the Ocean Protocol stack, allowing you to take advantage of decentralized asset file hosting. This article was originally posted on Medium in the Ocean Protocol blog. ✨ Going Decentralized With Ocean Protocol, you can use…",
- "frontmatter": {
- "title": "Ocean Protocol and IPFS, Sitting In The Merkle Tree",
-
- "linkurl": null,
- "tags": ["oceanprotocol", "blockchain", "design", "ipfs", "web3"],
- "image": {
- "childImageSharp": {
- "original": {
- "src": "/static/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser-f724bdaae38e81ec90b01ec6b8412cc8.png"
- },
- "fluid": {
- "aspectRatio": 2.3255813953488373,
- "src": "/static/f724bdaae38e81ec90b01ec6b8412cc8/687f7/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png",
- "srcSet": "/static/f724bdaae38e81ec90b01ec6b8412cc8/90a60/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png 100w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/c23b6/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png 200w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/687f7/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png 400w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/a9eb1/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png 600w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/9a629/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png 800w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/5956c/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png 2000w",
- "srcWebp": "/static/f724bdaae38e81ec90b01ec6b8412cc8/a93fc/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.webp",
- "srcSetWebp": "/static/f724bdaae38e81ec90b01ec6b8412cc8/b0720/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.webp 100w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/f6188/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.webp 200w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/a93fc/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.webp 400w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/7c0bb/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.webp 600w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/d1e4e/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.webp 800w,\n/static/f724bdaae38e81ec90b01ec6b8412cc8/eb8cf/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.webp 2000w",
- "sizes": "(max-width: 400px) 100vw, 400px",
- "originalImg": "/static/f724bdaae38e81ec90b01ec6b8412cc8/5956c/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png",
- "originalName": "ocean-protocol-and-ipfs-sitting-in-the-merkle-tree-teaser.png",
- "presentationWidth": 400,
- "presentationHeight": 140
- }
- }
- }
- },
- "fields": {
- "slug": "/ocean-protocol-and-ipfs-sitting-in-the-merkle-tree",
- "date": "October 24, 2019",
- "type": "post"
- }
- }
- }
- ]
- }
-}
diff --git a/.jest/__mocks__/file.js b/.jest/__mocks__/file.js
deleted file mode 100644
index 0e56c5b5..00000000
--- a/.jest/__mocks__/file.js
+++ /dev/null
@@ -1 +0,0 @@
-module.exports = 'test-file-stub'
diff --git a/.jest/__mocks__/gatsby.js b/.jest/__mocks__/gatsby.js
deleted file mode 100644
index cecf7bbd..00000000
--- a/.jest/__mocks__/gatsby.js
+++ /dev/null
@@ -1,29 +0,0 @@
-/* eslint-disable no-unused-vars */
-
-const React = require('react')
-const gatsby = jest.requireActual('gatsby')
-
-module.exports = {
- ...gatsby,
- graphql: jest.fn(),
- Link: jest.fn().mockImplementation(
- // these props are invalid for an `a` tag
- ({
- activeClassName,
- activeStyle,
- getProps,
- innerRef,
- partiallyActive,
- ref,
- replace,
- to,
- ...rest
- }) =>
- React.createElement('a', {
- ...rest,
- href: to
- })
- ),
- StaticQuery: jest.fn(),
- useStaticQuery: jest.fn()
-}
diff --git a/.jest/__mocks__/matchMedia.ts b/.jest/__mocks__/matchMedia.ts
deleted file mode 100644
index e4341f4a..00000000
--- a/.jest/__mocks__/matchMedia.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-const matchMediaMock = Object.defineProperty(window, 'matchMedia', {
- writable: true,
- value: jest
- .fn()
- .mockImplementationOnce((query) => ({
- matches: false,
- media: query,
- onchange: null,
- addListener: jest.fn(), // deprecated
- removeListener: jest.fn(), // deprecated
- addEventListener: jest.fn(),
- removeEventListener: jest.fn(),
- dispatchEvent: jest.fn()
- }))
- .mockImplementation((query) => ({
- matches: true,
- media: query,
- onchange: null,
- addListener: jest.fn(), // deprecated
- removeListener: jest.fn(), // deprecated
- addEventListener: jest.fn(),
- removeEventListener: jest.fn(),
- dispatchEvent: jest.fn()
- }))
-})
-
-export default matchMediaMock
diff --git a/.jest/__mocks__/svgr.js b/.jest/__mocks__/svgr.js
deleted file mode 100644
index c1faa04d..00000000
--- a/.jest/__mocks__/svgr.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import React from 'react'
-
-export default 'SvgrURL'
-const SvgrMock = React.forwardRef((props, ref) => )
-
-export const ReactComponent = SvgrMock
diff --git a/.jest/__mocks__/wagmi.js b/.jest/__mocks__/wagmi.js
deleted file mode 100644
index 4872c156..00000000
--- a/.jest/__mocks__/wagmi.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import { chain as chainOrig } from 'wagmi'
-
-export function useNetwork() {
- return {
- activeChain: {
- nativeCurrency: {
- symbol: 'ETH'
- }
- }
- }
-}
-
-export function useAccount() {
- return {
- data: {
- address: '0x0000000000000000000000000000000000000000'
- }
- }
-}
-
-export function useSendTransaction() {
- return {
- data: {
- address: '0x0000000000000000000000000000000000000000'
- }
- }
-}
-
-export function useEnsAvatar() {
- return {
- data: 'xxx.jpg'
- }
-}
-
-export function useEnsName() {
- return {
- data: 'fguhifgvewtyifgwyufew.eth'
- }
-}
-
-export function useBalance() {
- return {
- data: { formatted: '0.22', symbol: 'ETH' }
- }
-}
-
-export function useConnect() {
- return {
- connect: jest.fn()
- }
-}
-
-export function useDisconnect() {
- return {
- disconnect: jest.fn()
- }
-}
-
-export function useProvider() {
- return {}
-}
-
-export const chain = chainOrig
-
-export function createClient() {
- return {
- queryClient: {
- mount: jest.fn(),
- unmount: jest.fn()
- }
- }
-}
diff --git a/.jest/babel.config.js b/.jest/babel.config.js
deleted file mode 100644
index 88b4c6ea..00000000
--- a/.jest/babel.config.js
+++ /dev/null
@@ -1,4 +0,0 @@
-// this file only exists for Jest
-module.exports = {
- presets: ['babel-preset-gatsby', '@babel/preset-typescript']
-}
diff --git a/.jest/jest.config.js b/.jest/jest.config.js
deleted file mode 100644
index ab3f2407..00000000
--- a/.jest/jest.config.js
+++ /dev/null
@@ -1,58 +0,0 @@
-const esModules = [
- 'unified',
- 'vfile',
- 'vfile-.+',
- 'unist-.+',
- 'bail',
- 'is-plain-obj',
- 'trough',
- 'mdast-util-.+',
- 'micromark',
- 'micromark-.+',
- 'parse-entities',
- 'character-entities',
- 'property-information',
- 'comma-separated-tokens',
- 'hast-.+',
- 'remark-.+',
- 'rehype-.+',
- 'space-separated-tokens',
- 'trim-lines',
- 'decode-named-character-reference',
- 'ccount',
- 'escape-string-regexp',
- 'markdown-table',
- 'web-namespaces',
- '@rainbow-me/rainbowkit',
- 'wagmi',
- '@wagmi/chains',
- '@wagmi/core',
- '@wagmi/connectors',
- 'viem',
- 'devlop'
-].join('|')
-
-module.exports = {
- rootDir: '../',
- transform: {
- '^.+\\.[jt]sx?$': ['babel-jest', { configFile: './.jest/babel.config.js' }]
- },
- moduleNameMapper: {
- '.+\\.(css|styl|less|sass|scss)$': 'identity-obj-proxy',
- '.+\\.(jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$':
- '/.jest/__mocks__/file.js',
- '\\.svg': '/.jest/__mocks__/svgr.js',
- '^@reach/router(.*)': '/node_modules/@gatsbyjs/reach-router$1'
- },
- testPathIgnorePatterns: ['node_modules', '.cache', 'public', 'coverage'],
- transformIgnorePatterns: [
- `node_modules/(?!(gatsby|gatsby-link|gatsby-script|${esModules})/)`
- ],
- globals: {
- __PATH_PREFIX__: ''
- },
- setupFiles: ['/.jest/loadershim.js'],
- setupFilesAfterEnv: ['/.jest/setup-test-env.ts'],
- collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/@types/**/*'],
- testEnvironment: 'jsdom'
-}
diff --git a/.jest/loadershim.js b/.jest/loadershim.js
deleted file mode 100644
index 772dcc44..00000000
--- a/.jest/loadershim.js
+++ /dev/null
@@ -1,3 +0,0 @@
-global.___loader = {
- enqueue: jest.fn()
-}
diff --git a/.jest/setup-test-env.ts b/.jest/setup-test-env.ts
deleted file mode 100644
index 4d1b4712..00000000
--- a/.jest/setup-test-env.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import * as Gatsby from 'gatsby'
-import '@testing-library/jest-dom'
-import avatar from './__fixtures__/avatar.json'
-import github from './__fixtures__/github.json'
-import meta from './__fixtures__/meta.json'
-import posts from './__fixtures__/posts.json'
-import './__mocks__/matchMedia'
-
-// viem uses TextEncoder and TextDecoder which are not available with jsdom 16+
-import { TextEncoder, TextDecoder } from 'util'
-Object.assign(global, { TextDecoder, TextEncoder })
-
-const useStaticQuery = jest.spyOn(Gatsby, 'useStaticQuery')
-
-beforeAll(() => {
- useStaticQuery.mockImplementation(() => ({
- ...meta,
- ...avatar,
- logo: { edges: [{ node: { relativePath: 'apple-touch-icon.png' } }] },
- ...posts,
- ...github
- }))
-})
diff --git a/.jest/testRender.ts b/.jest/testRender.ts
deleted file mode 100644
index 041d0f68..00000000
--- a/.jest/testRender.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { ReactElement } from 'react'
-import { render } from '@testing-library/react'
-
-const testRender = (component: ReactElement): void => {
- it('renders without crashing', () => {
- const { container } = render(component)
-
- expect(container.firstChild).toBeInTheDocument()
- })
-}
-
-export default testRender
diff --git a/.prettierrc b/.prettierrc
deleted file mode 100644
index 3fd3e7b4..00000000
--- a/.prettierrc
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "semi": false,
- "singleQuote": true,
- "trailingComma": "none",
- "tabWidth": 2,
- "endOfLine": "lf",
- "importOrder": [
- "^(react/(.*)$)|^(react$)",
- "^(gatsby/(.*)$)|^(gatsby$)",
- "",
- "^[./]"
- ],
- "importOrderSeparation": false,
- "importOrderSortSpecifiers": true,
- "importOrderBuiltinModulesToTop": true,
- "importOrderMergeDuplicateImports": true,
- "importOrderCombineTypeAndValueImports": true
-}
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 00000000..33df0b2e
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,14 @@
+{
+ "semi": false,
+ "singleQuote": true,
+ "trailingComma": "none",
+ "tabWidth": 2,
+ "endOfLine": "lf",
+ "plugins": ["prettier-plugin-astro"],
+ "overrides": [
+ {
+ "files": "*.astro",
+ "options": { "parser": "astro" }
+ }
+ ]
+}
diff --git a/README.md b/README.md
index 78c79318..101719fb 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
- 🍭 My blog built with Gatsby + TypeScript. Neat.
+ 🍭 My blog built with Astro + TypeScript. Neat.
kremalicious.com
@@ -16,20 +16,22 @@
---
- [🎉 Features](#-features)
+ - [🌅 Image handling](#-image-handling)
- [🎆 EXIF extraction](#-exif-extraction)
- [💰 Cryptocurrency donation via Web3/MetaMask](#-cryptocurrency-donation-via-web3metamask)
- [🔍 Search](#-search)
- [🕸 Related Posts](#-related-posts)
- [📝 GitHub changelog rendering](#-github-changelog-rendering)
- [🌗 Theme Switcher](#-theme-switcher)
- - [🏆 SEO component](#-seo-component)
- - [gatsby-redirect-from](#gatsby-redirect-from)
- - [💎 Importing SVG assets](#-importing-svg-assets)
- - [🍬 Typekit component](#-typekit-component)
+ - [💎 SVG assets as components](#-svg-assets-as-components)
+ - [`redirect_from`](#redirect_from)
+ - [RSS \& JSON feeds](#rss--json-feeds)
- [✨ Development](#-development)
- [🔮 Linting](#-linting)
+ - [🔮 Type Checking](#-type-checking)
- [👩🔬 Testing](#-testing)
- - [🎈 Add a new post](#-add-a-new-post)
+- [🎈 Content creation helpers](#-content-creation-helpers)
+ - [Add a new post](#add-a-new-post)
- [🚚 Deployment](#-deployment)
- [S3 Deployment](#s3-deployment)
- [🏛 Licenses](#-licenses)
@@ -40,119 +42,120 @@
## 🎉 Features
-The whole [blog](https://kremalicious.com) is a React-based Single Page App built with [Gatsby v2](https://www.gatsbyjs.org).
+The whole [blog](https://kremalicious.com) is a statically exported site built with [Astro](https://astro.build) and TypeScript. Almost all components are Astro or native Web Components, with some React components loaded client-side.
+
+Styling happens through a combination of basic global styles and on components level either through CSS modules or CSS in `
+{title ? ({title} ) : ''}${innerSVG} `
+
+export const toInnerSvg = (input: string) =>
+ optimizeSVGNative(input, {
+ plugins: [
+ 'removeDoctype',
+ 'removeXMLProcInst',
+ 'removeComments',
+ 'removeMetadata',
+ 'removeXMLNS',
+ 'removeEditorsNSData',
+ 'cleanupAttrs',
+ 'minifyStyles',
+ 'convertStyleToAttrs',
+ 'cleanupIds',
+ 'removeRasterImages',
+ 'removeUselessDefs',
+ 'cleanupNumericValues',
+ 'cleanupListOfValues',
+ 'convertColors',
+ 'removeUnknownsAndDefaults',
+ 'removeNonInheritableGroupAttrs',
+ 'removeUselessStrokeAndFill',
+ 'removeViewBox',
+ 'cleanupEnableBackground',
+ 'removeHiddenElems',
+ 'removeEmptyText',
+ 'convertShapeToPath',
+ 'moveElemsAttrsToGroup',
+ 'moveGroupAttrsToElems',
+ 'collapseGroups',
+ 'convertPathData',
+ 'convertTransform',
+ 'removeEmptyAttrs',
+ 'removeEmptyContainers',
+ 'mergePaths',
+ 'removeUnusedNS',
+ 'sortAttrs',
+ 'removeTitle',
+ 'removeDesc',
+ 'removeDimensions',
+ 'removeStyleElement',
+ 'removeScriptElement'
+ ]
+ })
+ .data.replace(/^]*>|<\/svg>$/g, '')
+ .replace(/ fill="currentColor"/g, '')
+ .replace(/ (clip|fill)-rule="evenodd"/g, '')
+ .replace(/\/>/g, ' />')
diff --git a/scripts/create-symlinks.sh b/scripts/create-symlinks.sh
new file mode 100755
index 00000000..bbe6107f
--- /dev/null
+++ b/scripts/create-symlinks.sh
@@ -0,0 +1,26 @@
+#!/bin/bash
+
+paths=(
+ "content:src/content"
+ "content/articles/2012-07-16-using-kbd-for-fun-and-profit/_post-kbd.css:public/post-kbd.css"
+ "src/components/ThemeSwitch/theme.cjs:public/theme.js"
+)
+
+# Initialize a counter for created symlinks
+symlink_count=0
+
+# Iterate over the array and create symlinks
+for path_pair in "${paths[@]}"; do
+ IFS=":" read -r source target <<< "$path_pair"
+
+ # Check if the target symlink file or directory already exists
+ if [ -e "$PWD/$target" ]; then
+ continue
+ fi
+
+ # Create the symlink if it doesn't exist
+ ln -s "$PWD/$source" "$PWD/$target"
+ symlink_count=$((symlink_count + 1))
+done
+
+echo "✔️ [create-symlinks] Created $symlink_count symlinks."
diff --git a/scripts/deploy-s3.sh b/scripts/deploy-s3.sh
index c755c793..2e02a3a4 100755
--- a/scripts/deploy-s3.sh
+++ b/scripts/deploy-s3.sh
@@ -5,42 +5,31 @@
# AWS_SECRET_ACCESS_KEY
# AWS_DEFAULT_REGION
AWS_S3_BUCKET="kremalicious.com"
-SITEMAP_URL="https%3A%2F%2Fkremalicious.com%2Fsitemap.xml"
+SITEMAP_URL="https%3A%2F%2Fkremalicious.com%2Fsitemap-index.xml"
#
set -e;
function s3sync {
- aws s3 sync ./public s3://"$1" \
+ aws s3 sync ./dist s3://"$1" \
--include "*" \
--exclude "*.html" \
+ --exclude "*.zip" \
--exclude "sw.js" \
- --exclude "*page-data.json" \
- --exclude "*app-data.json" \
- --exclude "chunk-map.json" \
- --exclude "sitemap.xml" \
- --exclude "feed.xml" \
- --exclude "feed.json" \
- --exclude ".iconstats.json" \
- --exclude "humans.txt" \
- --exclude "robots.txt" \
+ --exclude "*.json" \
+ --exclude "*.txt" \
--cache-control public,max-age=31536000,immutable \
--delete \
--acl public-read
- aws s3 sync ./public s3://"$1" \
+ aws s3 sync ./dist s3://"$1" \
--exclude "*" \
--include "*.html" \
+ --include "*.zip" \
--include "sw.js" \
- --include "*page-data.json" \
- --include "*app-data.json" \
- --include "chunk-map.json" \
- --include "sitemap.xml" \
- --include "feed.xml" \
- --include "feed.json" \
- --include ".iconstats.json" \
- --include "humans.txt" \
- --include "robots.txt" \
+ --include "*.xml" \
+ --include "*.json" \
+ --include "*.txt" \
--cache-control public,max-age=0,must-revalidate \
--delete \
--acl public-read
diff --git a/scripts/move-downloads.test.ts b/scripts/move-downloads.test.ts
new file mode 100644
index 00000000..3d5d334c
--- /dev/null
+++ b/scripts/move-downloads.test.ts
@@ -0,0 +1,43 @@
+import { test, expect, vi } from 'vitest'
+import fs from 'node:fs/promises'
+import path from 'node:path'
+import { glob } from 'glob'
+import { copyZipFiles } from './move-downloads'
+import { fileURLToPath } from 'node:url'
+import chalk from 'chalk'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+
+test('copyZipFiles should copy zip files', async () => {
+ // Create temporary directories and files
+ const sourceDir = path.join(__dirname, 'tmp_source')
+ const destDir = path.join(__dirname, 'tmp_dest')
+ await fs.mkdir(sourceDir, { recursive: true })
+ await fs.mkdir(destDir, { recursive: true })
+ await fs.writeFile(path.join(sourceDir, 'file1.zip'), 'content1')
+ await fs.writeFile(path.join(sourceDir, 'file2.zip'), 'content2')
+
+ const globMock = vi.spyOn(glob, 'sync')
+ globMock.mockReturnValue(['file1.zip', 'file2.zip'])
+
+ const mockOra = {
+ start: vi.fn(),
+ succeed: vi.fn(),
+ fail: vi.fn()
+ }
+
+ copyZipFiles(sourceDir, destDir, mockOra as any)
+
+ const file1 = await fs.readFile(path.join(destDir, 'file1.zip'), 'utf-8')
+ const file2 = await fs.readFile(path.join(destDir, 'file2.zip'), 'utf-8')
+ expect(file1).toBe('content1')
+ expect(file2).toBe('content2')
+
+ expect(mockOra.succeed).toHaveBeenCalledWith(
+ `${chalk.bold('[move-downloads]')} Copied 2 .zip files to ${destDir}`
+ )
+
+ // Cleanup
+ await fs.rm(sourceDir, { recursive: true, force: true })
+ await fs.rm(destDir, { recursive: true, force: true })
+})
diff --git a/scripts/move-downloads.ts b/scripts/move-downloads.ts
new file mode 100644
index 00000000..257c96ca
--- /dev/null
+++ b/scripts/move-downloads.ts
@@ -0,0 +1,73 @@
+//
+// Find all zip files in all article folders
+// and copy them to public/get/ folder.
+//
+import fs from 'node:fs'
+import path from 'node:path'
+import { glob } from 'glob'
+import ora, { type Ora } from 'ora'
+import chalk from 'chalk'
+
+const sourceFolder = './content/articles/'
+const destinationFolder = './public/get/'
+const filesGlob = '**/*.zip'
+
+const spinner = ora(
+ `${chalk.bold('[move-downloads]')} Finding and moving zip files`
+).start()
+
+function removeFolderContents(folderPath: string) {
+ if (fs.existsSync(folderPath)) {
+ fs.readdirSync(folderPath).forEach((file) => {
+ const filePath = path.join(folderPath, file)
+ if (fs.lstatSync(filePath).isDirectory()) {
+ removeFolderContents(filePath)
+ fs.rmdirSync(filePath)
+ } else {
+ fs.unlinkSync(filePath)
+ }
+ })
+ }
+}
+
+export function copyZipFiles(
+ source: string,
+ destination: string,
+ spinner: Ora
+) {
+ // Clean out the destination folder
+ removeFolderContents(destination)
+
+ // Create the destination folder if it doesn't exist
+ if (!fs.existsSync(destination)) {
+ fs.mkdirSync(destination, { recursive: true })
+ }
+
+ // Find all files recursively in the source folder
+ const zipFiles = glob.sync(filesGlob, { cwd: source })
+
+ zipFiles.forEach((zipFile: string) => {
+ const sourcePath = path.join(source, zipFile)
+ const destinationPath = path.join(destination, path.basename(zipFile))
+
+ try {
+ // Copy the file to the destination folder
+ fs.copyFileSync(sourcePath, destinationPath)
+ } catch (error: any) {
+ spinner.fail(
+ `${chalk.bold('[move-downloads]')} Error copying ${zipFile}: ${
+ (error as Error).message
+ }`
+ )
+ return
+ }
+ })
+
+ spinner.succeed(
+ `${chalk.bold('[move-downloads]')} Copied ${
+ zipFiles.length
+ } .zip files to ${destination}`
+ )
+}
+
+copyZipFiles(sourceFolder, destinationFolder, spinner)
diff --git a/scripts/new.ts b/scripts/new.ts
deleted file mode 100644
index bc4f8877..00000000
--- a/scripts/new.ts
+++ /dev/null
@@ -1,119 +0,0 @@
-import fastExif from 'fast-exif'
-import fs from 'fs-extra'
-import iptc from 'node-iptc'
-import ora from 'ora'
-import path from 'path'
-import slugify from 'slugify'
-
-const templatePath = path.join(__dirname, 'new-article.md')
-const templatePathPhoto = path.join(__dirname, 'new-photo.md')
-const template = fs.readFileSync(templatePath).toString()
-const templatePhoto = fs.readFileSync(templatePathPhoto).toString()
-
-const spinner = ora('Adding new post').start()
-
-if (!process.argv[2]) {
- spinner.fail('Use the format `npm run new "Title of post"`')
-}
-
-let title = process.argv[2]
-const isPhoto = process.argv[2] === 'photo'
-
-spinner.text = `Adding '${title}'.`
-
-let titleSlug = slugify(title, { lower: true })
-const postsPath = path.join('.', 'content', 'articles')
-const photosPath = path.join('.', 'content', 'photos')
-
-let date = new Date().toISOString()
-
-async function getIptc(imagePath: string) {
- return new Promise((resolve, reject) => {
- fs.readFile(imagePath, (err, data) => {
- if (err) reject(err)
- const iptcData = iptc(data)
- return resolve(iptcData)
- })
- })
-}
-
-async function getExif(imagePath: string) {
- let exifData
- try {
- exifData = await fastExif.read(imagePath, true)
- } catch (error) {
- return null
- }
-
- let iptcData: any = {}
- try {
- iptcData = await getIptc(imagePath)
- } catch (error) {
- return null
- }
-
- return { ...exifData, iptc: { ...iptcData } }
-}
-
-async function createPhotoPost() {
- const photo = process.argv[3]
- try {
- const exifData = await getExif(photo)
- title = exifData.iptc.object_name || exifData.iptc.title
- titleSlug = slugify(title, { lower: true })
- date = new Date(exifData.exif.DateTimeOriginal).toISOString()
- const dateShort = date.slice(0, 10)
- const description = exifData.iptc.caption
- const fileName = `${dateShort}-${titleSlug}`
- const postPhoto = `${photosPath}/${fileName}.md`
-
- const newContentsPhoto = templatePhoto
- .split('TITLE')
- .join(title)
- .split('SLUG')
- .join(titleSlug)
- .split('DATE_LONG')
- .join(date)
- .split('DATE_SHORT')
- .join(dateShort)
- .split('DESCRIPTION')
- .join(description)
-
- // copy photo file in place
- fs.copyFile(photo, `${photosPath}/${fileName}.jpg`, (err) => {
- if (err) spinner.fail(`Error copying photo file: ${err}`)
- })
-
- // create photo post file
- fs.appendFile(postPhoto, newContentsPhoto, (err) => {
- if (err) spinner.fail(`Error creating photo post: ${err}`)
- spinner.succeed(`New photo post '${title}' as '${fileName}.md' created.`)
- })
- } catch (error) {
- console.error(error.message)
- }
-}
-
-if (isPhoto) {
- createPhotoPost()
-} else {
- if (process.argv[3]) {
- date = new Date(process.argv[3]).toISOString()
- }
-
- const dateShort = date.slice(0, 10)
- const file = `${postsPath}/${dateShort}-${titleSlug}/index.md`
-
- const newContents = template
- .split('TITLE')
- .join(title)
- .split('SLUG')
- .join(titleSlug)
- .split('DATE')
- .join(date)
-
- fs.outputFile(file, newContents)
- .then(() => fs.readFile(file, 'utf8'))
- .then(() => spinner.succeed(`New post '${title}' created.`))
- .catch((err: Error) => spinner.fail(`Error creating post: ${err.message}`))
-}
diff --git a/scripts/new/createArticlePost.ts b/scripts/new/createArticlePost.ts
new file mode 100644
index 00000000..74c4be75
--- /dev/null
+++ b/scripts/new/createArticlePost.ts
@@ -0,0 +1,52 @@
+import fs from 'node:fs/promises'
+import { existsSync, mkdirSync, readFileSync } from 'node:fs'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import slugify from '../../src/lib/slugify.js'
+import type { Ora } from 'ora'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+const templatePath = path.join(__dirname, 'new-article.md')
+
+export async function createArticlePost(
+ dest: string,
+ spinner: Ora,
+ title: string,
+ newDate?: string
+) {
+ let file
+ const date = newDate
+ ? new Date(newDate).toISOString()
+ : new Date().toISOString()
+
+ spinner.text = `Adding '${title}'.`
+
+ try {
+ const titleSlug = slugify(title)
+ const dateShort = date.slice(0, 10)
+ const folderName = `${dateShort}-${titleSlug}`
+ const destination = `${dest}/${folderName}`
+ file = `${destination}/index.md`
+ const template = readFileSync(templatePath).toString()
+ const newContents = template
+ .split('TITLE')
+ .join(title)
+ .split('SLUG')
+ .join(titleSlug)
+ .split('DATE')
+ .join(date)
+
+ // Create the destination folder if it doesn't exist
+ if (!existsSync(destination)) {
+ mkdirSync(destination, { recursive: true })
+ }
+
+ // create post file
+ await fs.appendFile(file, newContents)
+ spinner.succeed(`New post '${title}' created.`)
+ } catch (error: any) {
+ spinner.fail((error as Error).message)
+ }
+
+ return file
+}
diff --git a/scripts/new/createPhotoPost.ts b/scripts/new/createPhotoPost.ts
new file mode 100644
index 00000000..e2d8c3fc
--- /dev/null
+++ b/scripts/new/createPhotoPost.ts
@@ -0,0 +1,77 @@
+import fs from 'node:fs/promises'
+import { existsSync, mkdirSync, readFileSync } from 'node:fs'
+import slugify from '../../src/lib/slugify.js'
+import { readOutExif } from '../../src/lib/exif/index.js'
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import type { Ora } from 'ora'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+const templatePathPhoto = path.join(__dirname, 'new-photo.md')
+
+export async function createPhotoPost(
+ dest: string,
+ spinner: Ora,
+ photo: string,
+ photoTitle?: string
+) {
+ let title
+ let titleSlug
+ let date
+ let postPhotoFile
+
+ try {
+ const templatePhoto = readFileSync(templatePathPhoto).toString()
+ const exifData = await readOutExif(photo)
+ if (!exifData) throw new Error('No exif data found in image')
+ const { iptc, exif } = exifData
+
+ title = iptc?.object_name || iptc?.title || photoTitle
+ if (!title)
+ throw new Error(
+ 'No title given. Add to IPTC, or use the format `npm run new photo path/to/photo.jpg "Title of post"'
+ )
+ spinner.text = `Adding '${title}'.`
+
+ titleSlug = slugify(title)
+ date = new Date(exif?.date).toISOString()
+ const dateShort = date.slice(0, 10)
+ const description = iptc?.caption
+ const keywords = (iptc?.keywords as string[])?.join(`\n - `)
+ const folderName = `${dateShort}-${titleSlug}`
+ const destination = `${dest}/${folderName}`
+ postPhotoFile = `${destination}/index.md`
+
+ const newContentsPhoto = templatePhoto
+ .split('TITLE')
+ .join(title)
+ .split('SLUG')
+ .join(titleSlug)
+ .split('DATE_LONG')
+ .join(date)
+ .split('DATE_SHORT')
+ .join(dateShort)
+ .split('DESCRIPTION')
+ .join(description)
+ .split('TAGS')
+ .join(keywords)
+
+ // Create the destination folder if it doesn't exist
+ if (!existsSync(destination)) {
+ mkdirSync(destination, { recursive: true })
+ }
+
+ // copy photo file in place
+ await fs.copyFile(photo, `${destination}/${folderName}.jpg`)
+
+ // create photo post file
+ await fs.appendFile(postPhotoFile, newContentsPhoto)
+ spinner.succeed(
+ `New photo post '${title}' under '${postPhotoFile}' created.`
+ )
+ } catch (error: any) {
+ spinner.fail((error as Error).message)
+ }
+
+ return postPhotoFile
+}
diff --git a/scripts/new/index.test.ts b/scripts/new/index.test.ts
new file mode 100644
index 00000000..e9fb1e92
--- /dev/null
+++ b/scripts/new/index.test.ts
@@ -0,0 +1,78 @@
+import { test, expect, describe, afterEach } from 'vitest'
+import { createPhotoPost } from './createPhotoPost'
+import { promises as fs } from 'node:fs'
+import path from 'node:path'
+import type { Ora } from 'ora'
+import { createArticlePost } from './createArticlePost'
+
+const destFolder = path.join('.', 'test/__fixtures__/tmp')
+
+describe('npm run new', () => {
+ afterEach(async () => {
+ await fs.rmdir(destFolder, { recursive: true })
+ })
+
+ // Mock spinner
+ const spinner = {
+ text: '',
+ succeed: (text: string) => {
+ spinner.text = text
+ },
+ fail: (text: string) => {
+ spinner.text = text
+ }
+ } as Ora
+
+ test('createArticlePost should create a new article post', async () => {
+ const fixturePath = path.join('.', 'test/__fixtures__/new-article.md')
+
+ const title = 'Hello Test'
+ const date = '2023-09-10'
+ const file = await createArticlePost(destFolder, spinner, title, date)
+ expect(file).toBeDefined()
+ expect(spinner.text).toContain(`New post 'Hello Test' created.`)
+
+ // Verify that the article post was created
+ const fileExists =
+ file &&
+ (await fs
+ .access(file)
+ .then(() => true)
+ .catch(() => false))
+
+ expect(fileExists).toBe(true)
+
+ // Compare the generated index.md with the fixture new-article.md
+ const generatedContent = file && (await fs.readFile(file, 'utf8'))
+ const fixtureContent = await fs.readFile(fixturePath, 'utf8')
+ expect(generatedContent?.trim()).toBe(fixtureContent.trim())
+ })
+
+ test('createPhotoPost should create a new photo post', async () => {
+ const photoPath = path.join(
+ '.',
+ 'test/__fixtures__/image-with-metadata.jpg'
+ )
+ const fixturePath = path.join('.', 'test/__fixtures__/new-photo.md')
+
+ const postPhotoFile = await createPhotoPost(destFolder, spinner, photoPath)
+ expect(postPhotoFile).toBeDefined()
+
+ // Verify that the photo post was created
+ const fileExists =
+ postPhotoFile &&
+ (await fs
+ .access(postPhotoFile)
+ .then(() => true)
+ .catch(() => false))
+
+ expect(fileExists).toBe(true)
+ expect(spinner.text).toContain(`New photo post`)
+
+ // Compare the generated index.md with the fixture new-photo.md
+ const generatedContent =
+ postPhotoFile && (await fs.readFile(postPhotoFile, 'utf8'))
+ const fixtureContent = await fs.readFile(fixturePath, 'utf8')
+ expect(generatedContent?.trim()).toBe(fixtureContent.trim())
+ })
+})
diff --git a/scripts/new/index.ts b/scripts/new/index.ts
new file mode 100644
index 00000000..efa83918
--- /dev/null
+++ b/scripts/new/index.ts
@@ -0,0 +1,26 @@
+import path from 'node:path'
+import ora from 'ora'
+import { createPhotoPost } from './createPhotoPost.js'
+import { createArticlePost } from './createArticlePost.js'
+
+const postsPath = path.join('.', 'content', 'articles')
+const photosPath = path.join('.', 'content', 'photos')
+const spinner = ora('Adding new post').start()
+
+if (!process.argv[2]) {
+ spinner.fail(
+ 'Use the format `npm run new "Title of post"` or `npm run new photo path/to/photo.jpg`'
+ )
+}
+
+const isPhoto = process.argv[2] === 'photo'
+
+if (isPhoto) {
+ const photo = process.argv[3]
+ const photoTitle = process.argv[4]
+ createPhotoPost(photosPath, spinner, photo, photoTitle)
+} else {
+ const title = process.argv[2]
+ const newDate = process.argv[3]
+ createArticlePost(postsPath, spinner, title, newDate)
+}
diff --git a/scripts/new-article.md b/scripts/new/new-article.md
similarity index 70%
rename from scripts/new-article.md
rename to scripts/new/new-article.md
index 8de58bff..d4dddd81 100644
--- a/scripts/new-article.md
+++ b/scripts/new/new-article.md
@@ -2,7 +2,7 @@
date: DATE
title: TITLE
-image: SLUG-teaser.png
+image: ./SLUG-teaser.png
tags:
- tag
diff --git a/scripts/new-photo.md b/scripts/new/new-photo.md
similarity index 53%
rename from scripts/new-photo.md
rename to scripts/new/new-photo.md
index a1887487..ca624249 100644
--- a/scripts/new-photo.md
+++ b/scripts/new/new-photo.md
@@ -2,7 +2,10 @@
date: DATE_LONG
title: TITLE
-image: DATE_SHORT-SLUG.jpg
+image: ./DATE_SHORT-SLUG.jpg
+
+tags:
+ - TAGS
---
DESCRIPTION
diff --git a/scripts/redirect-from.test.ts b/scripts/redirect-from.test.ts
new file mode 100644
index 00000000..fa1a6a9a
--- /dev/null
+++ b/scripts/redirect-from.test.ts
@@ -0,0 +1,32 @@
+import { test, expect, vi } from 'vitest'
+import fs from 'node:fs/promises'
+import { Stats } from 'node:fs'
+import { findMarkdownFilesWithRedirects } from './redirect-from'
+
+test('findMarkdownFilesWithRedirects should generate correct redirects', async () => {
+ const readdirMock = vi.spyOn(fs, 'readdir')
+ readdirMock.mockResolvedValue(['post1.md', 'post2.md'] as any)
+
+ const statMock = vi.spyOn(fs, 'stat')
+ statMock.mockResolvedValue({ isFile: () => true } as Stats)
+
+ const readFileMock = vi.spyOn(fs, 'readFile')
+ readFileMock.mockResolvedValueOnce(
+ '---\nredirect_from: ["/old1", "/old2"]\nslug: /new1\n---'
+ )
+ readFileMock.mockResolvedValueOnce(
+ '---\nredirect_from: ["/old3"]\nslug: /new2\n---'
+ )
+
+ // Mock fs.writeFile to do nothing
+ const writeFileMock = vi.spyOn(fs, 'writeFile')
+ writeFileMock.mockResolvedValue()
+
+ const redirects = await findMarkdownFilesWithRedirects('some/dir')
+
+ expect(redirects).toEqual({
+ '/old1': '/new1',
+ '/old2': '/new1',
+ '/old3': '/new2'
+ })
+})
diff --git a/scripts/redirect-from.ts b/scripts/redirect-from.ts
new file mode 100644
index 00000000..88f46aac
--- /dev/null
+++ b/scripts/redirect-from.ts
@@ -0,0 +1,76 @@
+//
+// astro-redirect-from
+//
+import fs from 'node:fs/promises'
+import path from 'node:path'
+import frontmatter from 'front-matter'
+import ora from 'ora'
+import chalk from 'chalk'
+
+const contentDir = 'content/'
+const outputFilePath = '.config/redirects.json'
+let fileCount = 0
+
+type Frontmatter = { redirect_from?: string[]; slug?: string }
+
+const spinner = ora(
+ `${chalk.bold('[redirect-from]')} Extract redirects`
+).start()
+
+export async function findMarkdownFilesWithRedirects(
+ dir: string
+): Promise<{ [old: string]: string }> {
+ const redirects: { [old: string]: string } = {}
+
+ async function processDir(currentDir: string) {
+ const items = await fs.readdir(currentDir, { recursive: true })
+
+ for (const item of items) {
+ const itemPath = path.join(currentDir, item)
+ const stats = await fs.stat(itemPath)
+
+ if (
+ stats.isFile() &&
+ item.endsWith('.md') &&
+ !itemPath.includes('links')
+ ) {
+ const fileContent = await fs.readFile(itemPath, 'utf-8')
+ const { attributes }: { attributes: Frontmatter } =
+ frontmatter(fileContent)
+
+ // construct slug from frontmatter or folder name
+ const postSlug =
+ attributes.slug || `/${itemPath.split('/')[2].substring(11)}`
+
+ // Check if the Markdown file has a redirect_from field
+ if (attributes.redirect_from) {
+ fileCount++
+ const redirectFromSlugs = attributes.redirect_from
+ for (const slug of redirectFromSlugs) {
+ // Add entries to the redirects object
+ redirects[slug] = postSlug
+ }
+ }
+ }
+ }
+ }
+
+ await processDir(dir)
+ return redirects
+}
+
+try {
+ const redirects = await findMarkdownFilesWithRedirects(contentDir)
+ const redirectsJSON = JSON.stringify(redirects, null, 2)
+
+ // Write the redirects object to the output file
+ fs.writeFile(outputFilePath, redirectsJSON, 'utf-8')
+
+ spinner.succeed(
+ `${chalk.bold('[redirect-from]')} Extracted ${
+ Object.keys(redirects).length
+ } redirects from ${fileCount} files to ${outputFilePath}`
+ )
+} catch (error: any) {
+ spinner.fail(`${chalk.bold('[redirect-from]')} ${(error as Error).message}`)
+}
diff --git a/src/@types/Image.d.ts b/src/@types/Image.d.ts
deleted file mode 100644
index b18d0cdc..00000000
--- a/src/@types/Image.d.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { GatsbyImageProps } from 'gatsby-plugin-image'
-
-export interface ImageProps extends GatsbyImageProps {
- title?: string
- original?: { src: string }
- className?: string
-}
diff --git a/src/@types/Post.d.ts b/src/@types/Post.d.ts
deleted file mode 100644
index 8269a44b..00000000
--- a/src/@types/Post.d.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-export interface PageContext {
- tag?: string
- slug: string
- currentPageNumber: number
- numPages: number
- prevPagePath?: string
- nextPagePath?: string
-}
diff --git a/src/@types/css.d.ts b/src/@types/css.d.ts
deleted file mode 100644
index 6c344ed3..00000000
--- a/src/@types/css.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-declare module '*.module.css'
diff --git a/src/@types/node_modules.d.ts b/src/@types/dmsdec/index.d.ts
similarity index 63%
rename from src/@types/node_modules.d.ts
rename to src/@types/dmsdec/index.d.ts
index 6a834d57..517a478b 100644
--- a/src/@types/node_modules.d.ts
+++ b/src/@types/dmsdec/index.d.ts
@@ -1,8 +1,3 @@
-declare module 'pigeon-maps'
-declare module 'pigeon-marker'
-declare module 'unified'
-declare module 'node-iptc'
-
declare module 'dms2dec' {
export default function dms2dec(
lat: readonly number[],
diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts
index 3eb792c0..df1e7dce 100644
--- a/src/@types/global.d.ts
+++ b/src/@types/global.d.ts
@@ -1,12 +1,3 @@
-declare module '*.svg' {
- import * as React from 'react'
- export const ReactComponent: React.FunctionComponent<
- React.SVGProps
- >
- const src: string
- export default src
-}
-
interface Window {
__LUNR__: {
readonly [language: string]: {
diff --git a/src/@types/node-iptc/index.d.ts b/src/@types/node-iptc/index.d.ts
new file mode 100644
index 00000000..1e532094
--- /dev/null
+++ b/src/@types/node-iptc/index.d.ts
@@ -0,0 +1,71 @@
+declare module 'node-iptc' {
+ export default function iptc(buffer: Buffer): IptcData
+}
+
+type IptcData = {
+ object_type_reference?: string
+ object_attribute_reference?: string
+ object_name?: string
+ edit_status?: string
+ editorial_update?: string
+ urgency?: string
+ subject_reference?: string
+ category?: string
+ supplemental_categories?: string[]
+ fixture_id?: string[]
+ keywords?: string[]
+ content_location_code?: string[]
+ content_location_name?: string[]
+ release_date?: string
+ release_time?: string
+ expiration_date?: string
+ expiration_time?: string
+ special_instructions?: string
+ action_advised?: string
+ reference_service?: string[]
+ reference_date?: string[]
+ reference_number?: string[]
+ date_created?: string
+ time_created?: string
+ digital_date_created?: string
+ digital_time_created?: string
+ originating_program?: string
+ program_version?: string
+ object_cycle?: string
+ by_line?: string[]
+ caption?: string // not in spec, but observed in situ
+ by_line_title?: string[]
+ city?: string
+ sub_location?: string
+ province_or_state?: string
+ country_or_primary_location_code?: string
+ country_or_primary_location_name?: string
+ original_transmission_reference?: string
+ headline?: string
+ credit?: string
+ source?: string
+ copyright_notice?: string
+ contact?: string
+ local_caption?: string
+ caption_writer?: string[]
+ rasterized_caption?: string
+ image_type?: string
+ image_orientation?: string
+ language_identifier?: string
+ audio_type?: string
+ audio_sampling_rate?: string
+ audio_sampling_resolution?: string
+ audio_duration?: string
+ audio_outcue?: string
+
+ job_id?: string
+ master_document_id?: string
+ short_document_id?: string
+ unique_document_id?: string
+ owner_id?: string
+
+ object_preview_file_format?: string
+ object_preview_file_format_version?: string
+ object_preview_data?: string
+ date_time?: Date
+}
diff --git a/src/components/BackButton.astro b/src/components/BackButton.astro
new file mode 100644
index 00000000..931c4fd0
--- /dev/null
+++ b/src/components/BackButton.astro
@@ -0,0 +1,24 @@
+---
+import { ChevronLeft } from '@images/components'
+---
+
+
+
+
+ Go Back
+
+
+
diff --git a/src/components/Changelog/index.astro b/src/components/Changelog/index.astro
new file mode 100644
index 00000000..2367d396
--- /dev/null
+++ b/src/components/Changelog/index.astro
@@ -0,0 +1,50 @@
+---
+import { markdownToHtml } from '@lib/markdown'
+import { getRepo } from '@lib/github'
+import styles from './index.module.css'
+
+type Props = {
+ repo: string
+}
+
+const { repo } = Astro.props as Props
+const repoInfo = await getRepo(repo)
+if (!repoInfo) {
+ console.info(`Repo ${repo} not found`)
+ return
+}
+const { url, owner, object } = repoInfo
+const changelogHtml = await markdownToHtml(
+ object.text.replace('### Changelog', '')
+)
+---
+
+{
+ repoInfo ? (
+ <>
+ Changelog
+
+
+ Show all
+
+ >
+ ) : null
+}
+
+
diff --git a/src/components/atoms/Changelog.module.css b/src/components/Changelog/index.module.css
similarity index 81%
rename from src/components/atoms/Changelog.module.css
rename to src/components/Changelog/index.module.css
index 810f708f..92b5d2fd 100644
--- a/src/components/atoms/Changelog.module.css
+++ b/src/components/Changelog/index.module.css
@@ -1,7 +1,14 @@
-.content {
+.changelog {
padding-left: calc(var(--spacer) / 2);
margin-left: calc(var(--spacer) / 2);
- border-left: var(--stroke-width) solid var(--border-color);
+ border-left: var(--border-width) solid var(--border-color);
+ max-height: 800px;
+ overflow: hidden;
+ padding-bottom: var(--spacer);
+}
+
+.changelog:global(.all) {
+ max-height: none;
}
.content h2,
@@ -19,7 +26,7 @@
height: 0.5rem;
border-radius: 50%;
display: inline-block;
- background: var(--color-headings);
+ background: var(--border-color);
position: absolute;
left: -1.275rem;
top: calc(var(--font-size-large) / 3);
@@ -58,7 +65,7 @@
}
.source {
- font-size: var(--font-size-mini);
+ font-size: var(--font-size-small);
font-family: var(--font-family-base);
font-weight: var(--font-weight-base);
padding-bottom: calc(var(--spacer) / 2);
@@ -74,7 +81,7 @@
}
.source code {
- font-size: calc(var(--font-size-mini) * 0.9);
+ font-size: calc(var(--font-size-small) * 0.9);
}
.source a:hover {
diff --git a/src/components/Copy.astro b/src/components/Copy.astro
new file mode 100644
index 00000000..a1e684a7
--- /dev/null
+++ b/src/components/Copy.astro
@@ -0,0 +1,63 @@
+---
+import { Copy as CopyIcon } from '@images/components'
+
+type Props = {
+ text: string
+}
+
+const { text } = Astro.props as Props
+---
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/atoms/Divider.module.css b/src/components/Divider.module.css
similarity index 85%
rename from src/components/atoms/Divider.module.css
rename to src/components/Divider.module.css
index e22babeb..116f698e 100644
--- a/src/components/atoms/Divider.module.css
+++ b/src/components/Divider.module.css
@@ -13,6 +13,6 @@
border-bottom: 1px dashed #fff;
}
-:global(.dark) .divider::before {
+:global([data-theme='dark']) .divider::before {
border-bottom-color: var(--brand-grey);
}
diff --git a/src/components/Donation/Coin.astro b/src/components/Donation/Coin.astro
new file mode 100644
index 00000000..882b506f
--- /dev/null
+++ b/src/components/Donation/Coin.astro
@@ -0,0 +1,42 @@
+---
+import Copy from '@components/Copy.astro'
+
+type Props = {
+ address: string
+ title: string
+}
+
+const { address, title }: Props = Astro.props
+---
+
+
+
+
+
{title}
+
+ {address}
+
+
+
diff --git a/src/components/Donation/Web3.tsx b/src/components/Donation/Web3.tsx
new file mode 100644
index 00000000..b0676b41
--- /dev/null
+++ b/src/components/Donation/Web3.tsx
@@ -0,0 +1,15 @@
+import Web3Donation from '@components/Donation/Web3Donation'
+import config from '@config/blog.config.ts'
+import { RainbowKitProvider } from '@rainbow-me/rainbowkit'
+import { WagmiConfig } from 'wagmi'
+import { wagmiConfig, chains, theme } from '@lib/rainbowkit'
+
+export default function Web3() {
+ return (
+
+
+
+
+
+ )
+}
diff --git a/src/components/molecules/Web3Donation/Alert.module.css b/src/components/Donation/Web3Donation/Alert.module.css
similarity index 100%
rename from src/components/molecules/Web3Donation/Alert.module.css
rename to src/components/Donation/Web3Donation/Alert.module.css
diff --git a/src/components/molecules/Web3Donation/Alert.test.tsx b/src/components/Donation/Web3Donation/Alert.test.tsx
similarity index 88%
rename from src/components/molecules/Web3Donation/Alert.test.tsx
rename to src/components/Donation/Web3Donation/Alert.test.tsx
index 959fc986..a3204dee 100644
--- a/src/components/molecules/Web3Donation/Alert.test.tsx
+++ b/src/components/Donation/Web3Donation/Alert.test.tsx
@@ -1,5 +1,5 @@
-import React from 'react'
import { render } from '@testing-library/react'
+import { describe, it } from 'vitest'
import Alert from './Alert'
describe('Alert', () => {
diff --git a/src/components/molecules/Web3Donation/Alert.tsx b/src/components/Donation/Web3Donation/Alert.tsx
similarity index 83%
rename from src/components/molecules/Web3Donation/Alert.tsx
rename to src/components/Donation/Web3Donation/Alert.tsx
index b3dc4f90..f9fe120d 100644
--- a/src/components/molecules/Web3Donation/Alert.tsx
+++ b/src/components/Donation/Web3Donation/Alert.tsx
@@ -1,5 +1,5 @@
-import React, { ReactElement } from 'react'
-import * as styles from './Alert.module.css'
+import { type ReactElement } from 'react'
+import styles from './Alert.module.css'
export function getTransactionMessage(transactionHash?: string): {
[key: string]: string
@@ -38,9 +38,9 @@ export default function Alert({
}): ReactElement {
return (
)
diff --git a/src/components/molecules/Web3Donation/Conversion.module.css b/src/components/Donation/Web3Donation/Conversion.module.css
similarity index 72%
rename from src/components/molecules/Web3Donation/Conversion.module.css
rename to src/components/Donation/Web3Donation/Conversion.module.css
index b241f8bf..cf0ca5a0 100644
--- a/src/components/molecules/Web3Donation/Conversion.module.css
+++ b/src/components/Donation/Web3Donation/Conversion.module.css
@@ -2,8 +2,8 @@
font-size: var(--font-size-mini);
color: var(--text-color-light);
text-align: left;
- margin-top: calc(var(--spacer) / 4);
- margin-left: calc(var(--spacer) * 1.4);
+ margin-top: 0;
+ margin-left: calc(var(--spacer) * 2.4);
animation: fadeIn 0.5s 0.8s ease-out backwards;
}
diff --git a/src/components/molecules/Web3Donation/Conversion.test.tsx b/src/components/Donation/Web3Donation/Conversion.test.tsx
similarity index 66%
rename from src/components/molecules/Web3Donation/Conversion.test.tsx
rename to src/components/Donation/Web3Donation/Conversion.test.tsx
index d0cef61c..6f386b8d 100644
--- a/src/components/molecules/Web3Donation/Conversion.test.tsx
+++ b/src/components/Donation/Web3Donation/Conversion.test.tsx
@@ -1,9 +1,9 @@
-import React from 'react'
import { render } from '@testing-library/react'
+import { describe, it } from 'vitest'
import Conversion from './Conversion'
describe('Conversion', () => {
it('renders without crashing', async () => {
- render( )
+ render( )
})
})
diff --git a/src/components/molecules/Web3Donation/Conversion.tsx b/src/components/Donation/Web3Donation/Conversion.tsx
similarity index 61%
rename from src/components/molecules/Web3Donation/Conversion.tsx
rename to src/components/Donation/Web3Donation/Conversion.tsx
index dee0f366..60c03571 100644
--- a/src/components/molecules/Web3Donation/Conversion.tsx
+++ b/src/components/Donation/Web3Donation/Conversion.tsx
@@ -1,17 +1,16 @@
-import React, { ReactElement, useEffect, useState } from 'react'
-import axios from 'axios'
-import { useNetwork } from 'wagmi'
-import * as styles from './Conversion.module.css'
+import { type ReactElement, useEffect, useState } from 'react'
+import styles from './Conversion.module.css'
export async function getFiat(
amount: number,
tokenId = 'ethereum'
): Promise<{ [key: string]: string }> {
const url = `https://api.coingecko.com/api/v3/simple/price?ids=${tokenId}&vs_currencies=eur%2Cusd`
- const response = await axios(url)
+ const response = await fetch(url)
+ const json = await response.json()
- if (!response) console.error(response.statusText)
- const { usd, eur } = response.data[tokenId]
+ if (!json) console.error(response.statusText)
+ const { usd, eur } = json[tokenId]
const dollar = (amount * usd).toFixed(2)
const euro = (amount * eur).toFixed(2)
@@ -19,12 +18,12 @@ export async function getFiat(
}
export default function Conversion({
- amount
+ amount,
+ symbol
}: {
amount: string
+ symbol: string
}): ReactElement {
- const { chain } = useNetwork()
-
const [conversion, setConversion] = useState({
euro: '0.00',
dollar: '0.00'
@@ -32,23 +31,18 @@ export default function Conversion({
const { dollar, euro } = conversion
useEffect(() => {
- if (!chain?.nativeCurrency?.symbol) return
-
async function getFiatResponse() {
try {
- const tokenId =
- chain?.nativeCurrency?.symbol === 'MATIC'
- ? 'matic-network'
- : 'ethereum'
+ const tokenId = symbol === 'MATIC' ? 'matic-network' : 'ethereum'
const { dollar, euro } = await getFiat(Number(amount), tokenId)
setConversion({ euro, dollar })
} catch (error) {
- console.error(error.message)
+ console.error((error as Error).message)
}
}
getFiatResponse()
- }, [amount, chain?.nativeCurrency?.symbol])
+ }, [amount, symbol])
return (
diff --git a/src/components/molecules/Web3Donation/InputGroup.module.css b/src/components/Donation/Web3Donation/InputGroup.module.css
similarity index 95%
rename from src/components/molecules/Web3Donation/InputGroup.module.css
rename to src/components/Donation/Web3Donation/InputGroup.module.css
index 0ed1d61c..9e1d324c 100644
--- a/src/components/molecules/Web3Donation/InputGroup.module.css
+++ b/src/components/Donation/Web3Donation/InputGroup.module.css
@@ -64,7 +64,7 @@
margin-left: -1rem;
}
-:global(.dark) .inputInput {
+:global([data-theme='dark']) .inputInput {
border-color: var(--border-color);
}
@@ -83,7 +83,7 @@
align-items: center;
}
-:global(.dark) .currency {
+:global([data-theme='dark']) .currency {
border-right-color: #000;
}
diff --git a/src/components/Donation/Web3Donation/InputGroup.test.tsx b/src/components/Donation/Web3Donation/InputGroup.test.tsx
new file mode 100644
index 00000000..bf735179
--- /dev/null
+++ b/src/components/Donation/Web3Donation/InputGroup.test.tsx
@@ -0,0 +1,39 @@
+import { fireEvent, render, screen } from '@testing-library/react'
+import { describe, it, expect, vi } from 'vitest'
+import InputGroup from './InputGroup'
+
+const setAmount = vi.fn()
+
+describe('InputGroup', () => {
+ it('renders without crashing', async () => {
+ render(
+
+ )
+
+ const input = await screen.findByRole('textbox')
+ const button = await screen.findByRole('button')
+ fireEvent.change(input, { target: { value: '3' } })
+ fireEvent.click(button)
+ expect(setAmount).toHaveBeenCalled()
+ })
+
+ it('renders disabled', async () => {
+ render(
+
+ )
+
+ const input = await screen.findByRole('textbox')
+ expect(input).toBeDefined()
+ expect(input.attributes.getNamedItem('disabled')).toBeDefined()
+ })
+})
diff --git a/src/components/molecules/Web3Donation/InputGroup.tsx b/src/components/Donation/Web3Donation/InputGroup.tsx
similarity index 59%
rename from src/components/molecules/Web3Donation/InputGroup.tsx
rename to src/components/Donation/Web3Donation/InputGroup.tsx
index 0468455d..ad192a94 100644
--- a/src/components/molecules/Web3Donation/InputGroup.tsx
+++ b/src/components/Donation/Web3Donation/InputGroup.tsx
@@ -1,19 +1,19 @@
-import React, { ReactElement } from 'react'
-import { useAccount, useNetwork } from 'wagmi'
-import Input from '../../atoms/Input'
+import { type ReactElement } from 'react'
+import Input from '@components/Input'
import Conversion from './Conversion'
-import * as styles from './InputGroup.module.css'
+import styles from './InputGroup.module.css'
export default function InputGroup({
amount,
+ isDisabled,
+ symbol,
setAmount
}: {
amount: string
+ isDisabled: boolean
+ symbol: string
setAmount(amount: string): void
}): ReactElement {
- const { address } = useAccount()
- const { chain } = useNetwork()
-
return (
<>
@@ -25,17 +25,17 @@ export default function InputGroup({
value={amount}
onChange={(e) => setAmount(e.target.value)}
className={styles.inputInput}
- disabled={!address}
+ disabled={isDisabled}
/>
- {chain?.nativeCurrency?.symbol || 'ETH'}
+ {symbol}
-
+
Make it rain
-
+
>
)
}
diff --git a/src/components/molecules/Web3Donation/index.module.css b/src/components/Donation/Web3Donation/index.module.css
similarity index 77%
rename from src/components/molecules/Web3Donation/index.module.css
rename to src/components/Donation/Web3Donation/index.module.css
index 05a3d627..0258b31f 100644
--- a/src/components/molecules/Web3Donation/index.module.css
+++ b/src/components/Donation/Web3Donation/index.module.css
@@ -1,9 +1,9 @@
.web3 {
- composes: container from '../../Layout.module.css';
- max-width: 20rem;
- margin-bottom: calc(var(--spacer) * 2);
+ margin-left: auto;
+ margin-right: auto;
+ max-width: 25rem;
+ width: 100%;
text-align: center;
- min-height: 165px;
}
.web3 > div:first-child {
@@ -13,6 +13,12 @@
margin-bottom: var(--spacer);
}
+/* connect button */
+.web3 > div:first-child > button:only-child {
+ margin-left: auto;
+ margin-right: auto;
+}
+
.message {
font-size: var(--font-size-small);
position: relative;
diff --git a/src/components/Donation/Web3Donation/index.test.tsx b/src/components/Donation/Web3Donation/index.test.tsx
new file mode 100644
index 00000000..815c0899
--- /dev/null
+++ b/src/components/Donation/Web3Donation/index.test.tsx
@@ -0,0 +1,27 @@
+import { test, expect } from 'vitest'
+import { render, fireEvent, screen, waitFor } from '@testing-library/react'
+import Web3Donation from '.'
+
+test('Web3Donation component', async () => {
+ render( )
+
+ const submitButton = screen.getByRole('button')
+ expect(submitButton).toBeInTheDocument()
+
+ const connectButton = screen.getByText('Connect Wallet')
+ expect(connectButton).toBeInTheDocument()
+
+ const input = screen.getByRole('textbox')
+ expect(input).toBeInTheDocument()
+
+ fireEvent.change(input, { target: { value: '1' } })
+ expect(input).toHaveValue('1')
+
+ // Simulate form submission
+ fireEvent.click(submitButton)
+
+ await waitFor(() => {
+ const alert = screen.getByText(/Waiting for network confirmation/i)
+ expect(alert).toBeInTheDocument()
+ })
+})
diff --git a/src/components/molecules/Web3Donation/index.tsx b/src/components/Donation/Web3Donation/index.tsx
similarity index 70%
rename from src/components/molecules/Web3Donation/index.tsx
rename to src/components/Donation/Web3Donation/index.tsx
index 41dd86ff..01f5a8f0 100644
--- a/src/components/molecules/Web3Donation/index.tsx
+++ b/src/components/Donation/Web3Donation/index.tsx
@@ -1,19 +1,27 @@
-import React, { ReactElement, useState } from 'react'
+import { type ReactElement, useState } from 'react'
import { useDebounce } from 'use-debounce'
import { parseEther } from 'viem'
-import { usePrepareSendTransaction, useSendTransaction } from 'wagmi'
+import {
+ useAccount,
+ useNetwork,
+ usePrepareSendTransaction,
+ useSendTransaction
+} from 'wagmi'
import { ConnectButton } from '@rainbow-me/rainbowkit'
import Alert, { getTransactionMessage } from './Alert'
import InputGroup from './InputGroup'
-import * as styles from './index.module.css'
+import styles from './index.module.css'
export default function Web3Donation({
address
}: {
address: string
}): ReactElement {
- const [amount, setAmount] = useState('0.01')
+ const { address: account } = useAccount()
+ const { chain } = useNetwork()
+
+ const [amount, setAmount] = useState('0.005')
const [debouncedAmount] = useDebounce(amount, 500)
const { config } = usePrepareSendTransaction({
@@ -33,10 +41,10 @@ export default function Web3Donation({
})
try {
- const result = await sendTransactionAsync()
+ const result = sendTransactionAsync && (await sendTransactionAsync())
if (isError) {
- throw new Error(null)
+ throw new Error(undefined)
}
setTransactionHash(result?.hash)
@@ -52,10 +60,12 @@ export default function Web3Donation({
})
}
} catch (error) {
- setMessage(null)
+ setMessage(undefined)
}
}
+ const isDisabled = !account
+
return (
) : (
-
+
)}
)
diff --git a/src/components/Exif/ExifData.astro b/src/components/Exif/ExifData.astro
new file mode 100644
index 00000000..979450bf
--- /dev/null
+++ b/src/components/Exif/ExifData.astro
@@ -0,0 +1,15 @@
+---
+type Props = {
+ title: string
+ value: string
+ icon: any
+}
+
+const { title, value, icon } = Astro.props
+const Icon = icon
+---
+
+
+
+ {value}
+
diff --git a/src/components/Exif/ExifMap.test.tsx b/src/components/Exif/ExifMap.test.tsx
new file mode 100644
index 00000000..06dbd726
--- /dev/null
+++ b/src/components/Exif/ExifMap.test.tsx
@@ -0,0 +1,20 @@
+import { describe, it } from 'vitest'
+import { render, screen } from '@testing-library/react'
+import ExifMap from './ExifMap'
+
+describe('ExifMap', () => {
+ it('renders without crashing', async () => {
+ render(
+
+
+
+
+
+
+ )
+
+ await screen.findByText(/wheel to zoom/)
+ })
+})
diff --git a/src/components/Exif/ExifMap.tsx b/src/components/Exif/ExifMap.tsx
new file mode 100644
index 00000000..9be73fb5
--- /dev/null
+++ b/src/components/Exif/ExifMap.tsx
@@ -0,0 +1,57 @@
+import { type ReactElement, useState, useEffect } from 'react'
+import { Map, Marker } from 'pigeon-maps'
+
+const mapbox =
+ (mapboxId: string) => (x: string, y: string, z: string, dpr: number) =>
+ `https://api.mapbox.com/styles/v1/mapbox/${mapboxId}/tiles/256/${z}/${x}/${y}${
+ dpr >= 2 ? '@2x' : ''
+ }?access_token=${import.meta.env.PUBLIC_MAPBOX_ACCESS_TOKEN}`
+
+const providers = {
+ light: mapbox('light-v10'),
+ dark: mapbox('dark-v10')
+}
+
+type Theme = 'light' | 'dark'
+
+export default function ExifMap({
+ gps
+}: {
+ gps: { latitude: number; longitude: number }
+}): ReactElement {
+ const theme = document?.documentElement?.getAttribute('data-theme') as Theme
+
+ const [zoom, setZoom] = useState(12)
+ const [provider, setProvider] = useState(() => providers[theme || 'dark'])
+
+ function handleThemeChange() {
+ const theme = document.documentElement.getAttribute('data-theme') as Theme
+ setProvider(() => providers[theme])
+ }
+
+ useEffect(() => {
+ if (!window) return
+
+ const toggle = document.querySelector('#toggle') as HTMLInputElement
+ toggle.addEventListener('change', handleThemeChange)
+ return () => toggle.removeEventListener('change', handleThemeChange)
+ }, [])
+
+ const zoomIn = () => setZoom(Math.min(zoom + 4, 20))
+ const { latitude, longitude } = gps
+
+ return (
+
+
+
+ )
+}
diff --git a/src/components/Exif/index.astro b/src/components/Exif/index.astro
new file mode 100644
index 00000000..66399e04
--- /dev/null
+++ b/src/components/Exif/index.astro
@@ -0,0 +1,47 @@
+---
+import type { Exif } from '@lib/exif/types'
+import {
+ Camera,
+ Crosshair,
+ Aperture,
+ Stopwatch,
+ Sun,
+ Maximize
+} from '@images/components'
+import ExifData from './ExifData.astro'
+import styles from './index.module.css'
+import ExifMap from './ExifMap.tsx'
+
+type Props = {
+ exif: Exif
+}
+
+const { model, focalLength, fstop, shutterspeed, exposure, iso, gps } =
+ Astro.props.exif.exif
+---
+
+
+
+ {model && }
+ {
+ focalLength && (
+
+ )
+ }
+ {fstop && }
+ {
+ shutterspeed && (
+
+ )
+ }
+ {exposure && }
+ {iso && }
+
+ {
+ gps?.latitude && (
+
+
+
+ )
+ }
+
diff --git a/src/components/atoms/Exif.module.css b/src/components/Exif/index.module.css
similarity index 89%
rename from src/components/atoms/Exif.module.css
rename to src/components/Exif/index.module.css
index c9387ba6..054a282b 100644
--- a/src/components/atoms/Exif.module.css
+++ b/src/components/Exif/index.module.css
@@ -1,5 +1,4 @@
.exif {
- margin-top: -3rem;
margin-bottom: calc(var(--spacer) * 2);
}
@@ -48,8 +47,8 @@
}
.map {
- composes: breakout from '../Layout.module.css';
- composes: frame from './Image.module.css';
+ composes: breakout from '@layouts/Base/index.module.css';
+ composes: frame from '@components/Picture/index.module.css';
height: 220px;
margin-top: calc(var(--spacer) * 2);
}
diff --git a/src/components/Footer/Networks.astro b/src/components/Footer/Networks.astro
new file mode 100644
index 00000000..24028c8c
--- /dev/null
+++ b/src/components/Footer/Networks.astro
@@ -0,0 +1,30 @@
+---
+import styles from './Networks.module.css'
+import { Twitter, Rss, Mastodon, Github, Jsonfeed } from '@images/components'
+
+type Props = {
+ links: string[]
+}
+
+const { links } = Astro.props
+---
+
+
+ {
+ links.map((link: string) => (
+
+ {link.includes('mas.to') ? (
+
+ ) : link.includes('twitter') ? (
+
+ ) : link.includes('github') ? (
+
+ ) : link.includes('feed.xml') ? (
+
+ ) : link.includes('feed.json') ? (
+
+ ) : null}
+
+ ))
+ }
+
diff --git a/src/components/molecules/Networks.module.css b/src/components/Footer/Networks.module.css
similarity index 100%
rename from src/components/molecules/Networks.module.css
rename to src/components/Footer/Networks.module.css
diff --git a/src/components/Footer/Vcard.astro b/src/components/Footer/Vcard.astro
new file mode 100644
index 00000000..7f83d58c
--- /dev/null
+++ b/src/components/Footer/Vcard.astro
@@ -0,0 +1,28 @@
+---
+import { Image } from 'astro:assets'
+import Networks from './Networks.astro'
+import avatar from '@images/avatar.jpg'
+import config from '@config/blog.config.ts'
+import styles from './Vcard.module.css'
+
+const { author, rss, jsonfeed } = config
+const { mastodon, twitter, github, name, url } = author
+const links = [mastodon, github, twitter, rss, jsonfeed]
+---
+
+
+
+
+ {config.siteDescription.replace(name, '')}{' '}
+
+ {name}
+
+
+
+
diff --git a/src/components/molecules/Vcard.module.css b/src/components/Footer/Vcard.module.css
similarity index 66%
rename from src/components/molecules/Vcard.module.css
rename to src/components/Footer/Vcard.module.css
index 30ce8650..34be7194 100644
--- a/src/components/molecules/Vcard.module.css
+++ b/src/components/Footer/Vcard.module.css
@@ -1,8 +1,10 @@
.avatar {
- composes: frame from '../atoms/Image.module.css';
+ composes: frame from '@components/Picture/index.module.css';
border: 2px solid transparent;
- border-radius: 50%;
+ border-radius: 50% !important;
margin-bottom: calc(var(--spacer) / 3);
+ width: 80px;
+ height: 80px;
}
.description {
diff --git a/src/components/Footer/index.astro b/src/components/Footer/index.astro
new file mode 100644
index 00000000..e033c6ff
--- /dev/null
+++ b/src/components/Footer/index.astro
@@ -0,0 +1,30 @@
+---
+import { Github, Bitcoin } from '@images/components'
+import Vcard from './Vcard.astro'
+import styles from './index.module.css'
+import config from '@config/blog.config'
+
+const year = new Date().getFullYear()
+const { name, url, github } = config.author
+---
+
+
diff --git a/src/components/organisms/Footer.module.css b/src/components/Footer/index.module.css
similarity index 100%
rename from src/components/organisms/Footer.module.css
rename to src/components/Footer/index.module.css
diff --git a/src/components/Hamburger.astro b/src/components/Hamburger.astro
new file mode 100644
index 00000000..2990b3ca
--- /dev/null
+++ b/src/components/Hamburger.astro
@@ -0,0 +1,84 @@
+---
+const { props } = Astro
+---
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Header/index.astro b/src/components/Header/index.astro
new file mode 100644
index 00000000..36989bad
--- /dev/null
+++ b/src/components/Header/index.astro
@@ -0,0 +1,21 @@
+---
+import Menu from '@components/Menu/index.astro'
+import Search from '@components/Search/index.astro'
+import ThemeSwitch from '@components/ThemeSwitch/index.astro'
+import { Logo } from '@images/components'
+import styles from './index.module.css'
+---
+
+
diff --git a/src/components/organisms/Header.module.css b/src/components/Header/index.module.css
similarity index 96%
rename from src/components/organisms/Header.module.css
rename to src/components/Header/index.module.css
index b7ce3f3c..42633b44 100644
--- a/src/components/organisms/Header.module.css
+++ b/src/components/Header/index.module.css
@@ -11,7 +11,7 @@
.headerContent {
padding: 0 calc(var(--spacer) / 1.5);
- max-width: var(--maxWidthContainer);
+ max-width: var(--maxWidthContent);
margin-left: auto;
margin-right: auto;
position: relative;
diff --git a/src/components/atoms/Input.module.css b/src/components/Input/index.module.css
similarity index 93%
rename from src/components/atoms/Input.module.css
rename to src/components/Input/index.module.css
index aa5cb779..3162105a 100644
--- a/src/components/atoms/Input.module.css
+++ b/src/components/Input/index.module.css
@@ -8,7 +8,7 @@
color: var(--input-color);
background-color: var(--input-bg);
background-image: none;
- border: var(--stroke-width) solid transparent;
+ border: var(--border-width) solid var(--border-color);
border-radius: var(--border-radius);
box-shadow: none;
transition: all ease-in-out 0.15s;
diff --git a/src/components/Input/index.test.tsx b/src/components/Input/index.test.tsx
new file mode 100644
index 00000000..95dfadd5
--- /dev/null
+++ b/src/components/Input/index.test.tsx
@@ -0,0 +1,8 @@
+import { render, screen } from '@testing-library/react'
+import { test } from 'vitest'
+import Input from '.'
+
+test('Input', async () => {
+ render( )
+ await screen.findByRole('textbox')
+})
diff --git a/src/components/atoms/Input.tsx b/src/components/Input/index.tsx
similarity index 64%
rename from src/components/atoms/Input.tsx
rename to src/components/Input/index.tsx
index 6504b585..5ac12543 100644
--- a/src/components/atoms/Input.tsx
+++ b/src/components/Input/index.tsx
@@ -1,5 +1,5 @@
-import React, { InputHTMLAttributes, ReactElement } from 'react'
-import * as styles from './Input.module.css'
+import { type InputHTMLAttributes, type ReactElement } from 'react'
+import styles from './index.module.css'
export default function Input({
className,
diff --git a/src/components/Layout.test.tsx b/src/components/Layout.test.tsx
deleted file mode 100644
index 2555b3c4..00000000
--- a/src/components/Layout.test.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import React from 'react'
-import testRender from '../../.jest/testRender'
-import Layout from './Layout'
-
-describe('Layout', () => {
- testRender(Hello )
-})
diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx
deleted file mode 100644
index 4ab07349..00000000
--- a/src/components/Layout.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import React, { ReactElement } from 'react'
-import * as styles from './Layout.module.css'
-import Typekit from './atoms/Typekit'
-import Footer from './organisms/Footer'
-import Header from './organisms/Header'
-
-export default function Layout({ children }: { children: any }): ReactElement {
- return (
- <>
-
-
-
-
- {children}
-
-
-
- >
- )
-}
diff --git a/src/components/Menu/index.astro b/src/components/Menu/index.astro
new file mode 100644
index 00000000..b5feb76f
--- /dev/null
+++ b/src/components/Menu/index.astro
@@ -0,0 +1,39 @@
+---
+import Hamburger from '@components/Hamburger.astro'
+import config from '@config/blog.config.ts'
+import styles from './index.module.css'
+
+const { menu } = config
+---
+
+
+
+
+
+
diff --git a/src/components/molecules/Menu.module.css b/src/components/Menu/index.module.css
similarity index 93%
rename from src/components/molecules/Menu.module.css
rename to src/components/Menu/index.module.css
index 96d6065f..9dd8d236 100644
--- a/src/components/molecules/Menu.module.css
+++ b/src/components/Menu/index.module.css
@@ -6,6 +6,10 @@
width: 100%;
}
+.menu[aria-hidden] {
+ display: none;
+}
+
.menu ul {
margin: 0;
padding: 0;
@@ -31,7 +35,6 @@
text-transform: uppercase;
margin: 0 calc(var(--spacer) / 4);
font-size: var(--font-size-small);
- text-shadow: 0 1px 0 rgba(255 255 255 / 50%);
padding: var(--padding-base-horizontal);
display: block;
text-align: center;
diff --git a/src/components/More.astro b/src/components/More.astro
new file mode 100644
index 00000000..7dd93585
--- /dev/null
+++ b/src/components/More.astro
@@ -0,0 +1,42 @@
+---
+import { ChevronRight } from '@images/components'
+
+type Props = {
+ href: string
+}
+
+const { href } = Astro.props
+---
+
+
+
+
+
+
+
diff --git a/src/components/Pagination/PageNumber.astro b/src/components/Pagination/PageNumber.astro
new file mode 100644
index 00000000..e35bba98
--- /dev/null
+++ b/src/components/Pagination/PageNumber.astro
@@ -0,0 +1,18 @@
+---
+import styles from './index.module.css'
+
+type Props = {
+ i: number
+ slug: string
+ current?: boolean
+}
+
+const { i, slug, current } = Astro.props
+
+const classes = current ? `${styles.number} ${styles.current}` : styles.number
+const link = i === 0 ? slug : `${slug}/${i + 1}`
+---
+
+
+ {i + 1}
+
diff --git a/src/components/Pagination/PrevNext.astro b/src/components/Pagination/PrevNext.astro
new file mode 100644
index 00000000..f9b0bf61
--- /dev/null
+++ b/src/components/Pagination/PrevNext.astro
@@ -0,0 +1,19 @@
+---
+import styles from './index.module.css'
+import { ChevronLeft, ChevronRight } from '@images/components'
+
+type Props = {
+ prevPagePath?: string
+ nextPagePath?: string
+}
+
+const { prevPagePath, nextPagePath } = Astro.props
+
+const link = prevPagePath || nextPagePath
+const rel = prevPagePath ? 'prev' : 'next'
+const title = prevPagePath ? 'Newer Posts' : 'Older Posts'
+---
+
+
+ {prevPagePath ? : }
+
diff --git a/src/components/Pagination/index.astro b/src/components/Pagination/index.astro
new file mode 100644
index 00000000..db4f142c
--- /dev/null
+++ b/src/components/Pagination/index.astro
@@ -0,0 +1,25 @@
+---
+import PageNumber from './PageNumber.astro'
+import PrevNext from './PrevNext.astro'
+import styles from './index.module.css'
+
+type Props = {
+ slug: string
+ currentPage: number
+ numPages: number
+}
+
+const { slug, currentPage, numPages } = Astro.props
+const isFirst = currentPage === 1
+const isLast = currentPage === numPages
+---
+
+
diff --git a/src/components/molecules/Pagination.module.css b/src/components/Pagination/index.module.css
similarity index 97%
rename from src/components/molecules/Pagination.module.css
rename to src/components/Pagination/index.module.css
index 4b666b70..5a0d471f 100644
--- a/src/components/molecules/Pagination.module.css
+++ b/src/components/Pagination/index.module.css
@@ -34,7 +34,6 @@
}
.current {
- composes: number;
cursor: default;
pointer-events: none;
color: var(--text-color-dimmed);
diff --git a/src/components/PhotoTeaser.astro b/src/components/PhotoTeaser.astro
new file mode 100644
index 00000000..f8f80649
--- /dev/null
+++ b/src/components/PhotoTeaser.astro
@@ -0,0 +1,43 @@
+---
+import type { CollectionEntry } from 'astro:content'
+import Picture from '@components/Picture/index.astro'
+
+type Props = { post: CollectionEntry<'photos'> }
+
+const { slug, data } = Astro.props.post
+const { title, image } = data
+---
+
+
+
+
+ {
+ image ? (
+
+
+
+ ) : null
+ }
+
diff --git a/src/components/Picture/index.astro b/src/components/Picture/index.astro
new file mode 100644
index 00000000..cf3321fc
--- /dev/null
+++ b/src/components/Picture/index.astro
@@ -0,0 +1,105 @@
+---
+import { getImage, type ImgAttributes } from 'astro:assets'
+import styles from './index.module.css'
+import type { ImageMetadata, ImageOutputFormat } from 'astro'
+
+type Props = ImgAttributes & {
+ src: ImageMetadata
+ width: number
+ height?: number
+ class?: string
+ objectFit?: boolean
+}
+
+const {
+ title,
+ alt,
+ src,
+ class: className,
+ width,
+ height,
+ objectFit
+} = Astro.props
+
+const formats: ImageOutputFormat[] = ['webp', 'jpg']
+const sizes = [
+ Math.round(Number(width) / 2),
+ Number(width),
+ Math.round(Number(width) * 2)
+]
+
+const originalSize = src.width
+const images: { format: ImageOutputFormat; size: number; src: string }[] = []
+
+const srcFallback = await getImage({
+ src,
+ width: Math.round(Number(width) / 2),
+ format: 'jpg'
+})
+
+for (const format of formats) {
+ for (const size of sizes) {
+ const image = await getImage({ src, width: size, format, quality: 75 })
+
+ // Check if the original image is smaller than the target size,
+ // if the image is smaller, add the original size instead
+ images.push({
+ format,
+ size: originalSize >= size ? size : originalSize,
+ src: image.src
+ })
+ }
+}
+
+// Generate srcSet strings
+const srcSetStrings = formats.reduce((acc: Record, format) => {
+ const filteredImages = images.filter((img) => img.format === format)
+ acc[format] = filteredImages
+ .map((img) => `${img.src} ${img.size}w`)
+ .join(', ')
+ return acc
+}, {})
+---
+
+
+ {
+ objectFit ? (
+
+
+
+ ) : null
+ }
+
+ {
+ formats
+ .filter((format) => format !== 'jpg')
+ .map((format) => (
+
+ ))
+ }
+
+
+
+ {title && {title} }
+
diff --git a/src/components/atoms/Image.module.css b/src/components/Picture/index.module.css
similarity index 77%
rename from src/components/atoms/Image.module.css
rename to src/components/Picture/index.module.css
index f9113f1b..718f1aaa 100644
--- a/src/components/atoms/Image.module.css
+++ b/src/components/Picture/index.module.css
@@ -1,5 +1,5 @@
.frame {
- border: var(--stroke-width) solid rgba(255 255 255 / 20%);
+ border: var(--border-width) solid rgba(255 255 255 / 20%);
border-radius: var(--border-radius);
overflow: hidden;
transition: 0.2s ease-out;
@@ -14,16 +14,20 @@ a:focus .frame {
.image {
composes: frame;
max-width: none;
- margin-top: calc(var(--spacer) * var(--line-height));
- margin-bottom: calc(var(--spacer) * var(--line-height));
position: relative;
display: block;
}
-/* single photo post teasers */
-.image:only-child {
- margin-top: 0;
- margin-bottom: 0;
+.objectFit img {
+ object-fit: cover;
+ transform: translateZ(0);
+ inset: 0;
+ height: 100%;
+ margin: 0;
+ max-width: none;
+ padding: 0;
+ position: absolute;
+ width: 100%;
}
.imageTitle {
diff --git a/src/components/PostTeaser/index.astro b/src/components/PostTeaser/index.astro
new file mode 100644
index 00000000..4efc97eb
--- /dev/null
+++ b/src/components/PostTeaser/index.astro
@@ -0,0 +1,38 @@
+---
+import PostTitle from '../layouts/Post/Title.astro'
+import styles from './index.module.css'
+import Picture from '@components/Picture/index.astro'
+import type { CollectionEntry } from 'astro:content'
+
+type Props = {
+ post: CollectionEntry<'articles' | 'links'>
+ hideDate?: boolean
+}
+
+const { post, hideDate } = Astro.props
+const { slug } = post
+const { title, date, updated } = post.data
+const { image } = post.data as CollectionEntry<'articles'>['data']
+---
+
+
+ {
+ image ? (
+
+ ) : null
+ }
+
+
+
diff --git a/src/components/molecules/PostTeaser.module.css b/src/components/PostTeaser/index.module.css
similarity index 78%
rename from src/components/molecules/PostTeaser.module.css
rename to src/components/PostTeaser/index.module.css
index 7adc397b..822a182a 100644
--- a/src/components/molecules/PostTeaser.module.css
+++ b/src/components/PostTeaser/index.module.css
@@ -3,7 +3,7 @@
padding-right: 0.2rem;
margin-top: calc(var(--spacer) / 3);
margin-bottom: 0;
- font-size: var(--font-size-base);
+ font-size: var(--font-size-h3);
transition: color 0.2s ease-out;
}
@@ -16,6 +16,10 @@
display: block;
}
+.post + .post {
+ margin-top: calc(var(--spacer) * var(--line-height));
+}
+
.post:hover {
text-decoration: none;
}
@@ -24,12 +28,12 @@
color: var(--link-color);
}
-.post figure {
- margin: 0;
+.image {
+ overflow: hidden;
}
.empty {
- composes: frame from '../atoms/Image.module.css';
+ composes: frame from '@components/Picture/index.module.css';
display: block;
min-height: 95px;
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAkAAAAJCAYAAADgkQYQAAAAAXNSR0IArs4c6QAAAERlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAA6ABAAMAAAABAAEAAKACAAQAAAABAAAACaADAAQAAAABAAAACQAAAAAvQpmhAAAAHElEQVQYGWNgoBL4T8gcggoIGcBA0ASCCmhsBQBhFwX7u70C8QAAAABJRU5ErkJggg==');
diff --git a/src/components/RelatedPosts/index.astro b/src/components/RelatedPosts/index.astro
new file mode 100644
index 00000000..2f942640
--- /dev/null
+++ b/src/components/RelatedPosts/index.astro
@@ -0,0 +1,65 @@
+---
+import type { CollectionEntry } from 'astro:content'
+import Fuse from 'fuse.js'
+import styles from './index.module.css'
+import PhotoTeaser from '@components/PhotoTeaser.astro'
+import PostTeaser from '@components/PostTeaser/index.astro'
+import { getAllPostsForSearch } from '@lib/astro'
+
+type Props = {
+ post: CollectionEntry<'articles' | 'photos' | 'links'>
+ isPhotos?: boolean
+}
+const { post, isPhotos } = Astro.props as Props
+const allPosts = await getAllPostsForSearch()
+
+// Configure fuse.js
+// https://fusejs.io/api/options.html
+const fuseOptions: Fuse.IFuseOptions<
+ CollectionEntry<'articles' | 'photos' | 'links'>
+> = {
+ keys: ['data.tags', 'data.title', 'data.lead', 'collection'],
+ useExtendedSearch: true,
+ minMatchCharLength: 3,
+ threshold: 0.8
+}
+// TODO; figure out how to remove any
+const allPostsWithoutCurrent = allPosts?.filter(
+ (post) => post.slug !== Astro.props.post.slug
+)
+const fuse = new Fuse(allPostsWithoutCurrent as any, fuseOptions)
+const relatedPosts = fuse
+ // https://www.fusejs.io/examples.html#extended-search
+ .search(
+ `${post?.data?.tags?.join(' | ')} | ${post?.data?.title} | ${
+ (post?.data as any)?.lead
+ } | ${post?.collection}`
+ )
+ .map((result) => result.item)
+ .slice(0, 6)
+---
+
+
+
+ Related {isPhotos ? 'Photos' : 'Posts'}{' '}
+
+
+
+ {
+ relatedPosts?.map((post) => (
+
+ {isPhotos ? (
+ } />
+ ) : (
+ }
+ hideDate
+ />
+ )}
+
+ ))
+ }
+
+
diff --git a/src/components/molecules/RelatedPosts.module.css b/src/components/RelatedPosts/index.module.css
similarity index 84%
rename from src/components/molecules/RelatedPosts.module.css
rename to src/components/RelatedPosts/index.module.css
index 535a705e..9db70c47 100644
--- a/src/components/molecules/RelatedPosts.module.css
+++ b/src/components/RelatedPosts/index.module.css
@@ -1,6 +1,7 @@
.title {
font-size: var(--font-size-h3);
margin-bottom: var(--spacer);
+ color: var(--text-color-light);
}
.relatedPosts ul {
@@ -35,21 +36,12 @@
margin-bottom: 0;
}
-.relatedPosts a {
- display: block;
-}
-
.relatedPosts a > div {
margin-bottom: 0;
}
-.relatedPosts a h4 {
- color: var(--text-color-light);
-}
-
-.relatedPosts a:hover h4,
-.relatedPosts a:focus h4 {
- color: var(--link-color);
+.relatedPosts a h1 {
+ font-size: var(--font-size-h4);
}
.button {
diff --git a/src/components/molecules/Search/SearchResultsEmpty.module.css b/src/components/Search/Results/Empty.module.css
similarity index 100%
rename from src/components/molecules/Search/SearchResultsEmpty.module.css
rename to src/components/Search/Results/Empty.module.css
diff --git a/src/components/molecules/Search/SearchResultsEmpty.tsx b/src/components/Search/Results/Empty.tsx
similarity index 50%
rename from src/components/molecules/Search/SearchResultsEmpty.tsx
rename to src/components/Search/Results/Empty.tsx
index 02f25252..09bd53b4 100644
--- a/src/components/molecules/Search/SearchResultsEmpty.tsx
+++ b/src/components/Search/Results/Empty.tsx
@@ -1,20 +1,20 @@
-import React, { ReactElement } from 'react'
-import { Results } from './SearchResults'
-import * as styles from './SearchResultsEmpty.module.css'
+import { type ReactElement } from 'react'
+import styles from './Empty.module.css'
+import type { Post } from '../Search'
const SearchResultsEmpty = ({
- searchQuery,
+ query,
results
}: {
- searchQuery: string
- results: Results[]
+ query: string
+ results: Post[] | undefined
}): ReactElement => (
diff --git a/src/components/molecules/Search/SearchResults.module.css b/src/components/Search/Results/index.module.css
similarity index 70%
rename from src/components/molecules/Search/SearchResults.module.css
rename to src/components/Search/Results/index.module.css
index 7bf4e091..9fe6a719 100644
--- a/src/components/molecules/Search/SearchResults.module.css
+++ b/src/components/Search/Results/index.module.css
@@ -10,7 +10,7 @@
}
.results {
- composes: container from '../../Layout.module.css';
+ composes: content from '@layouts/Base/index.module.css';
padding: var(--spacer) calc(var(--spacer) / 2);
margin-bottom: 0;
display: flex;
@@ -18,11 +18,12 @@
justify-content: space-between;
}
-@media (min-width: 60rem) {
- .results {
- padding-left: 0;
- padding-right: 0;
- }
+.post {
+ composes: post from '@components/PostTeaser/index.module.css';
+}
+
+.title {
+ composes: title from '@components/PostTeaser/index.module.css';
}
.results li {
@@ -45,19 +46,6 @@
margin-bottom: 0;
}
-.results a {
- display: block;
-}
-
-.results a > div {
- margin-bottom: 0;
-}
-
-.results a:hover h4,
-.results a:focus h4 {
- color: var(--link-color);
-}
-
@keyframes fadein {
0% {
opacity: 0;
diff --git a/src/components/Search/Results/index.tsx b/src/components/Search/Results/index.tsx
new file mode 100644
index 00000000..95942049
--- /dev/null
+++ b/src/components/Search/Results/index.tsx
@@ -0,0 +1,49 @@
+import type { ReactElement } from 'react'
+import ReactDOM from 'react-dom'
+import styles from './index.module.css'
+import ResultsEmpty from './Empty'
+import type { Post } from '../Search'
+
+function SearchResultsPure({
+ query,
+ results
+}: {
+ results: Post[] | undefined
+ query: string
+}) {
+ return (
+
+ {results && results.length > 0 ? (
+
+ ) : (
+
+ )}
+
+ )
+}
+
+export default function SearchResults({
+ query,
+ results
+}: {
+ query: string
+ results: Post[] | undefined
+}): ReactElement {
+ // creating portal to break out of DOM node we're in
+ // and render the results in content container
+ return ReactDOM.createPortal(
+ ,
+ document.querySelector('#document') as Element
+ )
+}
diff --git a/src/components/molecules/Search/SearchInput.module.css b/src/components/Search/Search.module.css
similarity index 66%
rename from src/components/molecules/Search/SearchInput.module.css
rename to src/components/Search/Search.module.css
index 51ea6d29..bfe45213 100644
--- a/src/components/molecules/Search/SearchInput.module.css
+++ b/src/components/Search/Search.module.css
@@ -1,3 +1,15 @@
+.search {
+ position: absolute;
+ left: calc(var(--spacer) / 2);
+ right: calc(var(--spacer) / 2);
+ top: calc(var(--spacer) / 3);
+ z-index: 10;
+}
+
+.searchInput {
+ width: 100%;
+}
+
.searchInput::-webkit-search-cancel-button {
display: none;
}
diff --git a/src/components/Search/Search.test.tsx b/src/components/Search/Search.test.tsx
new file mode 100644
index 00000000..b63c0ebf
--- /dev/null
+++ b/src/components/Search/Search.test.tsx
@@ -0,0 +1,64 @@
+import { test, expect, vi, afterEach, beforeEach } from 'vitest'
+import { render, fireEvent, waitFor, screen, act } from '@testing-library/react'
+import { isSearchOpen } from '@stores/search'
+import Search from './Search'
+
+let portalRoot: HTMLDivElement
+let unsubscribe: () => void
+let fetchSpy: any
+let storeState = false
+
+beforeEach(() => {
+ portalRoot = global.document.createElement('div')
+ portalRoot.setAttribute('id', 'document')
+ global.document.body.appendChild(portalRoot)
+
+ // Track the store's state
+ unsubscribe = isSearchOpen.subscribe((value) => {
+ storeState = value
+ })
+
+ // Mock fetch API
+ globalThis.fetch = async () => {
+ return {
+ json: () =>
+ Promise.resolve([{ data: { title: 'Test Post' }, slug: 'test-post' }])
+ } as Response
+ }
+
+ fetchSpy = vi.spyOn(globalThis, 'fetch')
+})
+
+afterEach(() => {
+ // Cleanup
+ portalRoot.remove()
+ unsubscribe()
+})
+
+test('Search component', async () => {
+ render( )
+
+ act(() => {
+ // Simulate opening the search
+ isSearchOpen.set(true)
+ })
+
+ // Wait for the fetch to complete
+ await waitFor(() => {
+ expect(fetchSpy).toHaveBeenCalled()
+ })
+
+ // Check if the search input appears
+ const searchInput = screen.getByPlaceholderText('Search everything')
+ expect(searchInput).toBeInTheDocument()
+
+ fireEvent.change(searchInput, { target: { value: 'Test' } })
+ expect(searchInput).toHaveValue('Test')
+ expect(screen.getByText('Test Post')).toBeInTheDocument()
+
+ const closeButton = screen.getByTitle('Close search')
+ fireEvent.click(closeButton)
+
+ // Check if the search is closed
+ await waitFor(() => expect(storeState).toBe(false))
+})
diff --git a/src/components/Search/Search.tsx b/src/components/Search/Search.tsx
new file mode 100644
index 00000000..8d715ec6
--- /dev/null
+++ b/src/components/Search/Search.tsx
@@ -0,0 +1,97 @@
+import { type ReactElement, useEffect, useState } from 'react'
+import Fuse from 'fuse.js'
+import { useStore } from '@nanostores/react'
+import { isSearchOpen } from '@stores/search'
+import SearchResults from './Results'
+import styles from './Search.module.css'
+import type { CollectionEntry } from 'astro:content'
+import Input from '@components/Input'
+
+export type Post = CollectionEntry<'articles' | 'links' | 'photos'>
+
+// Configure fuse.js
+// https://fusejs.io/api/options.html
+const fuseOptions = {
+ keys: ['data.title', 'data.lead', 'slug'],
+ includeMatches: true,
+ minMatchCharLength: 2,
+ threshold: 0.5
+}
+
+export default function Search(): ReactElement {
+ const $isSearchOpen = useStore(isSearchOpen)
+
+ const [query, setQuery] = useState('')
+ const [results, setResults] = useState()
+ const [allPosts, setAllPosts] = useState()
+
+ // fetch all post data on open
+ useEffect(() => {
+ if (!$isSearchOpen) return
+
+ fetch('/api/posts')
+ .then((res) => res.json())
+ .then((json) => setAllPosts(json))
+ }, [$isSearchOpen])
+
+ // Handle search and set results
+ const fuse = allPosts ? new Fuse(allPosts, fuseOptions) : null
+
+ useEffect(() => {
+ if (!query || query === '' || !fuse) {
+ setResults([])
+ return
+ }
+
+ const results = fuse
+ .search(query)
+ .map((result) => result.item)
+ .slice(0, 6)
+
+ setResults(results)
+ }, [query])
+
+ // animate closing of search
+ async function toggleSearch(): Promise {
+ isSearchOpen.set(!$isSearchOpen)
+ }
+
+ return $isSearchOpen ? (
+ <>
+
+
+
+ >
+ ) : (
+ <>>
+ )
+}
diff --git a/src/components/Search/index.astro b/src/components/Search/index.astro
new file mode 100644
index 00000000..36943270
--- /dev/null
+++ b/src/components/Search/index.astro
@@ -0,0 +1,50 @@
+---
+import { Search as SearchIcon } from '@images/components'
+import Search from './Search.tsx'
+---
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Tag.astro b/src/components/Tag.astro
new file mode 100644
index 00000000..0d73b1f1
--- /dev/null
+++ b/src/components/Tag.astro
@@ -0,0 +1,38 @@
+---
+type Props = {
+ name: string
+ url: string
+ count?: number
+ style?: any
+}
+
+const { name, url, count, style } = Astro.props
+---
+
+
+
+
+ {name}
+ {count && {count} }
+
diff --git a/src/components/ThemeSwitch/index.astro b/src/components/ThemeSwitch/index.astro
new file mode 100644
index 00000000..825480dc
--- /dev/null
+++ b/src/components/ThemeSwitch/index.astro
@@ -0,0 +1,23 @@
+---
+import { Sun, Moon } from '@images/components'
+import styles from './index.module.css'
+---
+
+
+
+ Toggle Theme
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/molecules/ThemeSwitch.module.css b/src/components/ThemeSwitch/index.module.css
similarity index 91%
rename from src/components/molecules/ThemeSwitch.module.css
rename to src/components/ThemeSwitch/index.module.css
index 5a440568..ab8a11c0 100644
--- a/src/components/molecules/ThemeSwitch.module.css
+++ b/src/components/ThemeSwitch/index.module.css
@@ -23,6 +23,10 @@
display: block;
}
+.themeSwitch [hidden] {
+ display: none !important;
+}
+
/* hide visually */
.checkbox [type='checkbox'],
.checkbox .label {
diff --git a/src/components/ThemeSwitch/theme.cjs b/src/components/ThemeSwitch/theme.cjs
new file mode 100644
index 00000000..342c7da0
--- /dev/null
+++ b/src/components/ThemeSwitch/theme.cjs
@@ -0,0 +1,74 @@
+const htmlEl = document.documentElement
+const themeToggle = document.querySelector('#toggle')
+const currentTheme = localStorage.getItem('theme')
+
+function getPreferTheme() {
+ if (currentTheme) return currentTheme
+
+ return window.matchMedia('(prefers-color-scheme: dark)').matches
+ ? 'dark'
+ : 'light'
+}
+
+function getThemeColor(theme) {
+ return theme === 'dark' ? '#1d2224' : '#e7eef4'
+}
+
+let themeValue = getPreferTheme()
+let themeColor = getThemeColor(themeValue)
+
+function setPreference() {
+ localStorage.setItem('theme', themeValue)
+ reflectPreference()
+}
+
+function reflectPreference() {
+ const sun = document.querySelector('#sun')
+ const moon = document.querySelector('#moon')
+ const metaThemeColor = document.querySelector('meta[name=theme-color]')
+ const metaThemeColorMs = document.querySelector(
+ 'meta[name=msapplication-TileColor]'
+ )
+
+ htmlEl?.setAttribute('data-theme', themeValue)
+ htmlEl?.setAttribute('data-theme-color', themeColor)
+ metaThemeColor?.setAttribute('content', themeColor)
+ metaThemeColorMs?.setAttribute('content', themeColor)
+
+ themeToggle?.setAttribute('checked', `${themeValue === 'dark'}`)
+
+ if (themeValue === 'dark') {
+ sun?.removeAttribute('hidden')
+ moon?.setAttribute('hidden', '')
+ } else {
+ sun?.setAttribute('hidden', '')
+ moon?.removeAttribute('hidden')
+ }
+}
+
+function themeInit() {
+ // set early so no page flashes / CSS is made aware
+ reflectPreference()
+
+ window.onload = () => {
+ // set on load so screen readers can get the latest value on the button
+ reflectPreference()
+
+ themeToggle?.addEventListener('change', () => {
+ themeValue = themeValue === 'light' ? 'dark' : 'light'
+ themeColor = getThemeColor(themeValue)
+ setPreference()
+ })
+
+ // sync with system changes
+ window
+ .matchMedia('(prefers-color-scheme: dark)')
+ .addEventListener('change', ({ matches: isDark }) => {
+ themeValue = isDark ? 'dark' : 'light'
+ themeColor = getThemeColor(themeValue)
+ setPreference()
+ })
+ }
+}
+
+themeInit()
diff --git a/src/components/ThemeSwitch/theme.test.ts b/src/components/ThemeSwitch/theme.test.ts
new file mode 100644
index 00000000..7768f9a3
--- /dev/null
+++ b/src/components/ThemeSwitch/theme.test.ts
@@ -0,0 +1,110 @@
+import { test, expect, beforeAll } from 'vitest'
+
+function resetDocument() {
+ globalThis.localStorage = {
+ getItem: () => undefined,
+ setItem: () => {}
+ } as any
+
+ document.documentElement.innerHTML = `
+
+
+
+
+
+
+
+
+
+
+ `
+}
+
+beforeAll(() => {
+ resetDocument()
+})
+
+test('data-theme attribute is set as quickly as possible', async () => {
+ globalThis.localStorage = {
+ getItem: () => 'dark', // or 'dark'/'light' to simulate saved theme
+ setItem: () => {}
+ } as any
+
+ globalThis.window.matchMedia = () =>
+ ({ matches: true, addEventListener: () => {} }) as any
+
+ await import('./theme.cjs')
+
+ // Check the data-theme attribute
+ const htmlEl = document.querySelector('html')
+ expect(htmlEl?.getAttribute('data-theme')).toBe('dark') // or 'light'
+})
+
+test('data-theme attribute is set based on localStorage', async () => {
+ globalThis.localStorage = {
+ getItem: () => 'dark',
+ setItem: () => {}
+ } as any
+
+ await import('./theme.cjs')
+
+ const htmlEl = document.querySelector('html')
+ expect(htmlEl?.getAttribute('data-theme')).toBe('dark')
+})
+
+test('data-theme attribute is set based on system preference', async () => {
+ globalThis.window.matchMedia = () =>
+ ({ matches: true, addEventListener: () => {} }) as any
+
+ await import('./theme.cjs')
+
+ const htmlEl = document.querySelector('html')
+ expect(htmlEl?.getAttribute('data-theme')).toBe('dark')
+})
+
+test('data-theme attribute changes on system preference change', async () => {
+ let changeCallback: any = () => {}
+ globalThis.window.matchMedia = () =>
+ ({
+ matches: false,
+ addEventListener: (_: any, callback: any) => {
+ changeCallback = callback
+ }
+ }) as any
+
+ await import('./theme.cjs')
+
+ // Simulate a system preference change
+ changeCallback({ matches: true })
+
+ const htmlEl = document.querySelector('html')
+ expect(htmlEl?.getAttribute('data-theme')).toBe('dark')
+})
+
+test('meta tags are updated', async () => {
+ globalThis.window.matchMedia = () =>
+ ({ matches: true, addEventListener: () => {} }) as any
+
+ await import('./theme.cjs')
+
+ const metaThemeColor = document.querySelector('meta[name=theme-color]')
+ const metaThemeColorMs = document.querySelector(
+ 'meta[name=msapplication-TileColor]'
+ )
+
+ expect(metaThemeColor?.getAttribute('content')).toBe('#1d2224')
+ expect(metaThemeColorMs?.getAttribute('content')).toBe('#1d2224')
+})
+
+test('sun and moon hidden attributes are updated', async () => {
+ globalThis.window.matchMedia = () =>
+ ({ matches: true, addEventListener: () => {} }) as any
+
+ await import('./theme.cjs')
+
+ const sun = document.querySelector('#sun')
+ const moon = document.querySelector('#moon')
+
+ expect(sun?.hasAttribute('hidden')).toBe(false)
+ expect(moon?.hasAttribute('hidden')).toBe(true)
+})
diff --git a/src/components/Time.astro b/src/components/Time.astro
new file mode 100644
index 00000000..5c91232f
--- /dev/null
+++ b/src/components/Time.astro
@@ -0,0 +1,20 @@
+---
+import { format, formatDistance } from 'date-fns'
+
+type Props = {
+ date: Date | undefined
+}
+
+const { date } = Astro.props
+---
+
+{
+ date ? (
+
+ {formatDistance(date, Date.now(), { addSuffix: true })}
+
+ ) : null
+}
diff --git a/src/components/Toc.astro b/src/components/Toc.astro
new file mode 100644
index 00000000..3511ab73
--- /dev/null
+++ b/src/components/Toc.astro
@@ -0,0 +1,29 @@
+---
+type Props = {
+ tableOfContents: string
+}
+
+const { tableOfContents } = Astro.props
+---
+
+
+
+
diff --git a/src/components/atoms/Changelog.test.tsx b/src/components/atoms/Changelog.test.tsx
deleted file mode 100644
index 3eb8f489..00000000
--- a/src/components/atoms/Changelog.test.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from 'react'
-import { render } from '@testing-library/react'
-import Changelog from './Changelog'
-
-describe('Changelog', () => {
- it('renders without crashing', () => {
- const { container, rerender } = render(
-
- )
- expect(container.firstChild).toBeInTheDocument()
-
- // return nothing when no match
- rerender( )
- expect(container.firstChild).not.toBeInTheDocument()
- })
-})
diff --git a/src/components/atoms/Changelog.tsx b/src/components/atoms/Changelog.tsx
deleted file mode 100644
index 772c324e..00000000
--- a/src/components/atoms/Changelog.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-import React, {
- Fragment,
- ReactElement,
- createElement,
- useEffect,
- useState
-} from 'react'
-import { graphql, useStaticQuery } from 'gatsby'
-import rehypeReact from 'rehype-react'
-import remarkParse from 'remark-parse'
-import remarkRehype from 'remark-rehype'
-import { unified } from 'unified'
-import * as styles from './Changelog.module.css'
-
-export function PureChangelog({
- repo,
- repos
-}: {
- repo: string
- repos: Queries.GitHubReposQuery['github']['viewer']['repositories']['edges']
-}): ReactElement | null {
- const [changelogHtml, setChangelogHtml] = useState()
-
- const repoMatch = repos
- .map(({ node }) => {
- if (node.name === repo) return node
- })
- .filter((n: any) => n)[0]
-
- useEffect(() => {
- if (!(repoMatch?.object as Queries.GitHub_Blob)?.text) return
-
- async function init() {
- const changelogHtml = await unified()
- .use(remarkParse)
- .use(remarkRehype)
- .use(rehypeReact, { createElement, Fragment })
- .processSync((repoMatch?.object as Queries.GitHub_Blob).text).result
-
- setChangelogHtml(changelogHtml)
- }
- init()
- }, [(repoMatch?.object as Queries.GitHub_Blob)?.text])
-
- return repoMatch ? (
-
- ) : null
-}
-
-const queryGithub = graphql`
- query GitHubRepos {
- github {
- viewer {
- repositories(first: 100, privacy: PUBLIC, isFork: false) {
- edges {
- node {
- name
- url
- owner {
- login
- }
- object(expression: "main:CHANGELOG.md") {
- id
- ... on GitHub_Blob {
- text
- }
- }
- }
- }
- }
- }
- }
- }
-`
-
-export default function Changelog({ repo }: { repo: string }): ReactElement {
- const data = useStaticQuery(queryGithub)
- const repos = data.github.viewer.repositories.edges
- return
-}
diff --git a/src/components/atoms/Copy.module.css b/src/components/atoms/Copy.module.css
deleted file mode 100644
index 8bec9649..00000000
--- a/src/components/atoms/Copy.module.css
+++ /dev/null
@@ -1,29 +0,0 @@
-.button {
- margin: 0;
- position: absolute;
- right: 0;
- top: 0;
- bottom: 0;
- border: 0;
- box-shadow: none;
- border-top-left-radius: 0;
- border-bottom-left-radius: 0;
- padding: calc(var(--spacer) / 3);
-}
-
-.button svg {
- stroke: var(--text-color-light);
- transition: 0.15s ease-out;
-}
-
-.copied {
- background: green;
-}
-
-.copied svg {
- stroke: var(--text-color-dimmed);
-}
-
-.button:hover svg {
- stroke: var(--text-color-dimmed);
-}
diff --git a/src/components/atoms/Copy.test.tsx b/src/components/atoms/Copy.test.tsx
deleted file mode 100644
index f99dfb94..00000000
--- a/src/components/atoms/Copy.test.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import React from 'react'
-import { fireEvent, render, screen } from '@testing-library/react'
-import Copy from './Copy'
-
-describe('Copy', () => {
- it('renders without crashing', () => {
- render( )
- fireEvent.click(screen.getByTitle('Copy to clipboard'))
- })
-})
diff --git a/src/components/atoms/Copy.tsx b/src/components/atoms/Copy.tsx
deleted file mode 100644
index a021d3fb..00000000
--- a/src/components/atoms/Copy.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React, { ReactElement } from 'react'
-import Clipboard from 'react-clipboard.js'
-import * as styles from './Copy.module.css'
-import Icon from './Icon'
-
-const onCopySuccess = (e: any) => {
- e.trigger.classList.add(styles.copied)
-}
-
-export default function Copy({ text }: { text: string }): ReactElement {
- return (
- onCopySuccess(e)}
- className={styles.button}
- >
-
-
- )
-}
diff --git a/src/components/atoms/Exif.test.tsx b/src/components/atoms/Exif.test.tsx
deleted file mode 100644
index b3cd560b..00000000
--- a/src/components/atoms/Exif.test.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from 'react'
-import { render } from '@testing-library/react'
-import Exif from './Exif'
-
-const exif: Partial = {
- formatted: {
- iso: '500',
- model: 'Canon',
- fstop: '7.2',
- shutterspeed: '200',
- focalLength: '200',
- lensModel: 'Hello',
- exposure: '200',
- gps: { latitude: 41.89007222222222, longitude: 12.491516666666666 }
- }
-}
-
-describe('Exif', () => {
- it('renders without crashing', () => {
- const { container } = render( )
-
- expect(container.firstChild).toBeInTheDocument()
- })
-})
diff --git a/src/components/atoms/Exif.tsx b/src/components/atoms/Exif.tsx
deleted file mode 100644
index 7d4645d6..00000000
--- a/src/components/atoms/Exif.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import React, { ReactElement } from 'react'
-import * as styles from './Exif.module.css'
-import ExifMap from './ExifMap'
-import Icon from './Icon'
-
-const ExifData = ({
- title,
- value,
- icon
-}: {
- title: string
- value: string
- icon: string
-}) => (
-
-
- {value}
-
-)
-
-export default function Exif({
- exif
-}: {
- exif: Queries.ImageExif
-}): ReactElement {
- const { iso, model, fstop, shutterspeed, focalLength, exposure, gps } =
- exif.formatted
-
- const formattedModel = model === 'FC7203' ? 'DJI Mavic Mini' : model
-
- return (
-
-
- {formattedModel && (
-
- )}
- {focalLength && (
-
- )}
- {fstop && }
- {shutterspeed && (
-
- )}
- {exposure && }
- {iso && }
-
- {gps?.latitude && (
-
-
-
- )}
-
- )
-}
diff --git a/src/components/atoms/ExifMap.tsx b/src/components/atoms/ExifMap.tsx
deleted file mode 100644
index 86b20a95..00000000
--- a/src/components/atoms/ExifMap.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import React, { ReactElement, useState } from 'react'
-import { Map } from 'pigeon-maps'
-import Marker from 'pigeon-marker'
-import useDarkMode from '../../hooks/useDarkMode'
-
-const mapbox =
- (mapboxId: string) => (x: string, y: string, z: string, dpr: number) =>
- `https://api.mapbox.com/styles/v1/mapbox/${mapboxId}/tiles/256/${z}/${x}/${y}${
- dpr >= 2 ? '@2x' : ''
- }?access_token=${process.env.GATSBY_MAPBOX_ACCESS_TOKEN}`
-
-const providers = {
- light: mapbox('light-v10'),
- dark: mapbox('dark-v10')
-}
-
-export default function ExifMap({
- gps
-}: {
- gps: { latitude: number; longitude: number }
-}): ReactElement {
- const { isDarkMode } = useDarkMode()
- const [zoom, setZoom] = useState(12)
-
- const zoomIn = () => {
- setZoom(Math.min(zoom + 4, 20))
- }
-
- const { latitude, longitude } = gps
-
- return (
-
-
-
- )
-}
diff --git a/src/components/atoms/Hamburger.module.css b/src/components/atoms/Hamburger.module.css
deleted file mode 100644
index 5d4e429a..00000000
--- a/src/components/atoms/Hamburger.module.css
+++ /dev/null
@@ -1,70 +0,0 @@
-.hamburger {
- width: 24px;
- height: 24px;
- display: inline-block;
- position: relative;
- transform: rotate(0deg);
- cursor: pointer;
- margin-top: calc(var(--spacer) / 2);
-}
-
-.line {
- display: block;
- position: absolute;
- height: 1px;
- width: 100%;
- border-bottom: var(--stroke-width) solid var(--text-color-light);
- opacity: 1;
- left: 0;
- transform: rotate(0deg);
- transition: 0.2s var(--easing);
-}
-
-.line:nth-child(1) {
- top: 0;
- transform-origin: left center;
-}
-
-.line:nth-child(2) {
- top: 7px;
- transform-origin: left center;
-}
-
-.line:nth-child(3) {
- top: 14px;
- transform-origin: left center;
-}
-
-/* open state */
-:global(.has-menu-open) .line:nth-child(1) {
- transform: rotate(45deg);
- top: -1px;
-}
-
-:global(.has-menu-open) .line:nth-child(2) {
- width: 0%;
- opacity: 0;
-}
-
-:global(.has-menu-open) .line:nth-child(3) {
- transform: rotate(-45deg);
- top: 16px;
-}
-
-.button {
- padding: calc(var(--spacer) / 2);
- vertical-align: middle;
- display: inline-block;
- margin: 0;
- margin-right: -1rem;
-}
-
-.button:hover,
-.button:focus {
- outline: 0;
-}
-
-.button:hover .line,
-.button:focus .line {
- border-color: var(--link-color);
-}
diff --git a/src/components/atoms/Hamburger.tsx b/src/components/atoms/Hamburger.tsx
deleted file mode 100644
index 55be7e8a..00000000
--- a/src/components/atoms/Hamburger.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import React, { ReactElement } from 'react'
-import * as styles from './Hamburger.module.css'
-
-export default function Hamburger({
- onClick
-}: {
- onClick(): void
-}): ReactElement {
- return (
-
-
-
-
-
-
-
- )
-}
diff --git a/src/components/atoms/HeadMeta/SchemaOrg.tsx b/src/components/atoms/HeadMeta/SchemaOrg.tsx
deleted file mode 100644
index f0a395f4..00000000
--- a/src/components/atoms/HeadMeta/SchemaOrg.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import React, { ReactElement } from 'react'
-import { ImageDataLike } from 'gatsby-plugin-image'
-import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
-
-type SchemaOrgProps = {
- post?: {
- title: string
- url: string
- image: ImageDataLike
- description: string
- datePublished: string
- dateModified: string
- }
-}
-
-export default function SchemaOrg({ post }: SchemaOrgProps): ReactElement {
- const { siteTitle, siteUrl, author } = useSiteMetadata()
-
- const schemaOrgJsonLd: any = [
- {
- '@context': 'http://schema.org',
- '@type': 'Blog',
- url: siteUrl,
- name: siteTitle
- }
- ]
-
- if (post) {
- schemaOrgJsonLd.push({
- '@context': 'http://schema.org',
- '@type': 'BlogPosting',
- author: {
- '@type': 'Person',
- name: author.name
- },
- publisher: {
- '@type': 'Organization',
- name: author.name
- },
- url: post.url,
- name: post.title,
- headline: post.title,
- image: {
- '@type': 'ImageObject',
- url: post.image
- },
- description: post.description,
- datePublished: post.datePublished,
- dateModified: post.dateModified || post.datePublished,
- mainEntityOfPage: {
- '@type': 'Blog',
- '@id': siteUrl
- }
- })
- }
-
- return (
-
- )
-}
diff --git a/src/components/atoms/HeadMeta/index.tsx b/src/components/atoms/HeadMeta/index.tsx
deleted file mode 100644
index a5a1eae5..00000000
--- a/src/components/atoms/HeadMeta/index.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import React, { ReactElement } from 'react'
-import { graphql, useStaticQuery } from 'gatsby'
-import { ImageDataLike, getSrc } from 'gatsby-plugin-image'
-import useDarkMode from '../../../hooks/useDarkMode'
-import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
-
-const query = graphql`
- query Logo {
- logo: allFile(filter: { name: { eq: "apple-touch-icon" } }) {
- edges {
- node {
- relativePath
- }
- }
- }
- }
-`
-
-export type HeadMetaProps = {
- title?: string
- description?: string
- image?: ImageDataLike
- slug: string
- children?: ReactElement
-}
-
-export default function HeadMeta(props: HeadMetaProps): ReactElement {
- const data = useStaticQuery(query)
- const logo = data.logo.edges[0].node.relativePath
- const { siteTitle, siteUrl, siteDescription, author } = useSiteMetadata()
- const { themeColor } = useDarkMode()
-
- const title = props.title
- ? `${props.title} ¦ ${siteTitle}`
- : `${siteTitle} ¦ ${siteDescription}`
- const description = props.description
- ? props.description.slice(0, 160)
- : siteDescription
- const url = props.slug ? `${siteUrl}${props.slug}` : siteUrl
- const image = props.image
- ? `${siteUrl}${getSrc(props.image)}`
- : `${siteUrl}${logo}`
-
- return (
- <>
-
-
- {title}
-
-
-
-
-
-
-
-
-
-
-
-
- {props.children}
- >
- )
-}
diff --git a/src/components/atoms/Icon.module.css b/src/components/atoms/Icon.module.css
deleted file mode 100644
index a7dee05e..00000000
--- a/src/components/atoms/Icon.module.css
+++ /dev/null
@@ -1,10 +0,0 @@
-.icon {
- width: 1em;
- height: 1em;
- stroke: currentcolor;
- stroke-width: var(--stroke-width);
- stroke-linecap: round;
- stroke-linejoin: round;
- fill: none;
- vertical-align: baseline;
-}
diff --git a/src/components/atoms/Icon.test.tsx b/src/components/atoms/Icon.test.tsx
deleted file mode 100644
index 7e1a9d3c..00000000
--- a/src/components/atoms/Icon.test.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import React from 'react'
-import { render } from '@testing-library/react'
-import Icon from './Icon'
-
-describe('Icon', () => {
- it('renders correctly', () => {
- const { container, rerender } = render( )
- expect(container.firstChild.nodeName).toBe('svg')
-
- rerender( )
- expect(container.firstChild.nodeName).toBe('svg')
-
- rerender( )
- expect(container.firstChild.nodeName).toBe('svg')
- })
-
- it('does not render with unknown name', () => {
- const { container } = render( )
- expect(container.firstChild).not.toBeInTheDocument()
- })
-})
diff --git a/src/components/atoms/Icon.tsx b/src/components/atoms/Icon.tsx
deleted file mode 100644
index 267de3d4..00000000
--- a/src/components/atoms/Icon.tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import React, { FunctionComponent, ReactElement } from 'react'
-// https://featherstyles.com
-// import * as Feather from '@kremalicious/react-feather'
-import {
- Aperture,
- ArrowDownCircle,
- Camera,
- ChevronLeft,
- ChevronRight,
- Compass,
- Copy,
- Crosshair,
- Edit,
- ExternalLink,
- GitHub,
- Link,
- Maximize,
- Moon,
- Rss,
- Search,
- Sun,
- Twitter,
- X
-} from 'react-feather'
-import { ReactComponent as Bitcoin } from '../../images/bitcoin.svg'
-// custom icons
-import { ReactComponent as Jsonfeed } from '../../images/jsonfeed.svg'
-import { ReactComponent as Mastodon } from '../../images/mastodon.svg'
-import { ReactComponent as Stopwatch } from '../../images/stopwatch.svg'
-import * as styles from './Icon.module.css'
-
-const components: {
- [key: string]: FunctionComponent>
-} = {
- Download: ArrowDownCircle,
- Jsonfeed,
- Bitcoin,
- Stopwatch,
- ArrowDownCircle,
- Edit,
- GitHub,
- Twitter,
- Rss,
- Sun,
- Moon,
- Compass,
- X,
- Copy,
- Search,
- ExternalLink,
- Link,
- ChevronRight,
- ChevronLeft,
- Camera,
- Aperture,
- Maximize,
- Crosshair,
- Mastodon
-}
-
-const Icon = ({ name, ...props }: { name: string }): ReactElement => {
- const IconMapped = components[name]
- // const IconFeather = (Feather as any)[name]
- if (!IconMapped) return null
-
- return
-}
-
-export default Icon
diff --git a/src/components/atoms/Image.tsx b/src/components/atoms/Image.tsx
deleted file mode 100644
index 3820a2a0..00000000
--- a/src/components/atoms/Image.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import React, { ReactElement } from 'react'
-import { graphql } from 'gatsby'
-import { GatsbyImage } from 'gatsby-plugin-image'
-import { ImageProps } from '../../@types/Image'
-import * as styles from './Image.module.css'
-
-export const Image = ({
- title,
- image,
- alt,
- original,
- className
-}: ImageProps): ReactElement => (
-
-
- {title && {title} }
-
-)
-
-export const imageSizeDefault = graphql`
- fragment ImageFluid on ImageSharp {
- original {
- src
- }
- gatsbyImageData(width: 1040)
- }
-`
-
-export const imageSizeThumb = graphql`
- fragment ImageFluidThumb on ImageSharp {
- original {
- src
- }
- gatsbyImageData(
- width: 480
- height: 180
- transformOptions: { cropFocus: CENTER }
- )
- }
-`
-
-export const photoSizeThumb = graphql`
- fragment PhotoFluidThumb on ImageSharp {
- original {
- src
- }
- gatsbyImageData(
- width: 316
- height: 316
- transformOptions: { cropFocus: CENTER }
- )
- }
-`
diff --git a/src/components/atoms/Input.test.tsx b/src/components/atoms/Input.test.tsx
deleted file mode 100644
index 78299d65..00000000
--- a/src/components/atoms/Input.test.tsx
+++ /dev/null
@@ -1,8 +0,0 @@
-import React from 'react'
-// import { render } from '@testing-library/react'
-import testRender from '../../../.jest/testRender'
-import Input from './Input'
-
-describe('Input', () => {
- testRender( )
-})
diff --git a/src/components/atoms/Tag.module.css b/src/components/atoms/Tag.module.css
deleted file mode 100644
index 07f62c31..00000000
--- a/src/components/atoms/Tag.module.css
+++ /dev/null
@@ -1,20 +0,0 @@
-.tag {
- color: var(--text-color-light);
- margin-left: calc(var(--spacer) / 2);
- margin-right: calc(var(--spacer) / 2);
- margin-bottom: calc(var(--spacer) / 2);
- white-space: nowrap;
- display: inline-block;
-}
-
-.tag::before {
- content: '#';
- margin-right: 1px;
- opacity: 0.65;
-}
-
-.count {
- font-size: var(--font-size-small);
- margin-left: calc(var(--spacer) / 6);
- opacity: 0.65;
-}
diff --git a/src/components/atoms/Tag.tsx b/src/components/atoms/Tag.tsx
deleted file mode 100644
index c3f08e82..00000000
--- a/src/components/atoms/Tag.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import React, { ReactElement } from 'react'
-import { Link } from 'gatsby'
-import * as styles from './Tag.module.css'
-
-export default function Tag({
- name,
- url,
- count,
- style
-}: {
- name: string
- url: string
- count?: number
- style?: any
-}): ReactElement {
- return (
-
- {name}
- {count && {count} }
-
- )
-}
diff --git a/src/components/atoms/Time.tsx b/src/components/atoms/Time.tsx
deleted file mode 100644
index acc4103f..00000000
--- a/src/components/atoms/Time.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import React, { ReactElement } from 'react'
-import { format, formatDistance } from 'date-fns'
-
-export default function Time({ date }: { date: string }): ReactElement {
- const dateNew = new Date(date)
- const dateIso = dateNew.toISOString()
-
- return (
-
- {formatDistance(dateNew, Date.now(), { addSuffix: true })}
-
- )
-}
diff --git a/src/components/atoms/Transitions.ts b/src/components/atoms/Transitions.ts
deleted file mode 100644
index ab5a6ea4..00000000
--- a/src/components/atoms/Transitions.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-export const moveInTop = {
- initial: {
- opacity: 0,
- y: -100,
- transition: { type: 'spring' }
- },
- enter: {
- opacity: 1,
- y: 0,
- transition: {
- type: 'spring',
- duration: 0.2,
- stiffness: 120
- }
- },
- exit: {
- opacity: 0,
- y: -100,
- transition: {
- type: 'spring',
- duration: 0.2
- }
- }
-}
-
-export function getAnimationProps(shouldReduceMotion: boolean) {
- return {
- initial: `${shouldReduceMotion ? 'enter' : 'initial'}`,
- animate: `${shouldReduceMotion ? null : 'enter'}`,
- exit: `${shouldReduceMotion ? null : 'exit'}`
- }
-}
diff --git a/src/components/atoms/Typekit.tsx b/src/components/atoms/Typekit.tsx
deleted file mode 100644
index 6d8de3a9..00000000
--- a/src/components/atoms/Typekit.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import React from 'react'
-import { Script } from 'gatsby'
-
-const script = `
- (function(d) {
- var config = {
- kitId: '${process.env.GATSBY_TYPEKIT_ID}',
- scriptTimeout: 3000,
- async: true
- },
- h=d.documentElement,t=setTimeout(function(){h.className=h.className.replace(/\bwf-loading\b/g,"")+" wf-inactive";},config.scriptTimeout),tk=d.createElement("script"),f=false,s=d.getElementsByTagName("script")[0],a;h.className+=" wf-loading";tk.src='https://use.typekit.net/'+config.kitId+'.js';tk.async=true;tk.onload=tk.onreadystatechange=function(){a=this.readyState;if(f||a&&a!="complete"&&a!="loaded")return;f=true;clearTimeout(t);try{Typekit.load(config)}catch(e){}};s.parentNode.insertBefore(tk,s)
- })(document);
-`
-
-export default function Typekit(): JSX.Element {
- return
-}
diff --git a/src/components/layouts/Archive.astro b/src/components/layouts/Archive.astro
new file mode 100644
index 00000000..95fe867f
--- /dev/null
+++ b/src/components/layouts/Archive.astro
@@ -0,0 +1,58 @@
+---
+import LayoutBase from '@layouts/Base/index.astro'
+import type { Page } from 'astro'
+import type { CollectionEntry } from 'astro:content'
+import PostTeaser from '@components/PostTeaser/index.astro'
+import Pagination from '@components/Pagination/index.astro'
+import PhotoTeaser from '@components/PhotoTeaser.astro'
+
+type Props = {
+ page: Page>
+ title: string
+ pageTitle: string
+}
+
+const { page, title, pageTitle } = Astro.props
+const classes = `posts ${
+ title && title !== '' && title.toLowerCase().includes('photos')
+ ? 'photos'
+ : ''
+}`
+---
+
+
+
+
+
+ {
+ page?.data?.map((post) =>
+ post.collection === 'photos' ? (
+
+ ) : (
+
+ )
+ )
+ }
+
+
+ {
+ page.currentPage && (
+
+ )
+ }
+
diff --git a/src/components/layouts/Base/Head.astro b/src/components/layouts/Base/Head.astro
new file mode 100644
index 00000000..335b0604
--- /dev/null
+++ b/src/components/layouts/Base/Head.astro
@@ -0,0 +1,119 @@
+---
+import { getImage } from 'astro:assets'
+import SchemaOrg, { type Props as SchemaProps } from './SchemaOrg.astro'
+import config from '@config/blog.config.ts'
+import { getUmamiConfig } from '@lib/umami'
+import faviconSrc from '@images/favicon.png'
+import faviconSvgSrc from '@images/favicon.svg'
+import { type Props as IndexProps } from './index.astro'
+
+type Props = IndexProps
+
+const { title, description, style, image, date, updated } = Astro.props
+
+const titleFinal =
+ title && title !== ''
+ ? `${title} ¦ ${config.siteTitle}`
+ : `${config.siteTitle} ¦ ${config.siteDescription}`
+
+const descriptionFinal = description
+ ? description.slice(0, 160)
+ : config.siteDescription
+
+const canonicalURL =
+ Astro.url.pathname === '/'
+ ? Astro.site?.origin
+ : `${Astro.site?.origin}${Astro.url.pathname}`
+
+const imageFinal = `${Astro.site?.origin}${
+ image
+ ? (await getImage({ src: image, width: 800, format: 'jpg' })).src
+ : faviconSrc.src
+}`
+
+const imageFinalAlt = image ? `Teaser image for ${title}` : `Logo`
+
+const schema: SchemaProps = {
+ title: titleFinal,
+ url: canonicalURL as string,
+ image: imageFinal,
+ description: descriptionFinal,
+ ...(date && { datePublished: date.toISOString().substring(0, 10) }),
+ ...(updated && { dateModified: updated.toISOString().substring(0, 10) })
+}
+
+const isProduction = import.meta.env.PROD
+const typekitID = import.meta.env.PUBLIC_TYPEKIT_ID
+const { UMAMI_SCRIPT_URL, UMAMI_WEBSITE_ID } = getUmamiConfig()
+
+const appleTouchIcon = await getImage({
+ src: faviconSrc,
+ width: 180,
+ height: 180,
+ format: 'png'
+})
+const faviconSvg = await getImage({ src: faviconSvgSrc, format: 'svg' })
+---
+
+
+
+
+
+
+
+ {titleFinal}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ typekitID && (
+
+ )
+ }
+
+ {style && }
+
+ {
+ isProduction && (
+
+ )
+ }
+
diff --git a/src/components/layouts/Base/SchemaOrg.astro b/src/components/layouts/Base/SchemaOrg.astro
new file mode 100644
index 00000000..32f87e73
--- /dev/null
+++ b/src/components/layouts/Base/SchemaOrg.astro
@@ -0,0 +1,51 @@
+---
+import config from '@config/blog.config.ts'
+
+export type Props = {
+ title: string
+ url: string
+ image: string
+ description: string
+ datePublished?: string
+ dateModified?: string
+}
+
+const { title, url, image, description, datePublished, dateModified } =
+ Astro.props
+const { siteUrl, siteTitle, author } = config
+
+const schema = datePublished
+ ? {
+ '@context': 'http://schema.org',
+ '@type': 'BlogPosting',
+ author: {
+ '@type': 'Person',
+ name: author.name
+ },
+ publisher: {
+ '@type': 'Organization',
+ name: author.name
+ },
+ url,
+ headline: title,
+ image: {
+ '@type': 'ImageObject',
+ url: image
+ },
+ description,
+ datePublished,
+ dateModified: dateModified || datePublished,
+ mainEntityOfPage: {
+ '@type': 'Blog',
+ '@id': siteUrl
+ }
+ }
+ : {
+ '@context': 'http://schema.org',
+ '@type': 'Blog',
+ url: siteUrl,
+ name: siteTitle
+ }
+---
+
+
diff --git a/src/components/layouts/Base/index.astro b/src/components/layouts/Base/index.astro
new file mode 100644
index 00000000..5368d4e5
--- /dev/null
+++ b/src/components/layouts/Base/index.astro
@@ -0,0 +1,35 @@
+---
+import '../../../styles/global.css'
+import '../../../styles/imports.css'
+
+import type { ImageMetadata } from 'astro'
+import type { CollectionEntry } from 'astro:content'
+import Header from '@components/Header/index.astro'
+import Footer from '@components/Footer/index.astro'
+import styles from './index.module.css'
+import Head from './Head.astro'
+
+export type Props = CollectionEntry<'articles' | 'links' | 'photos'>['data'] & {
+ pageTitle?: string
+ description?: string
+ image?: ImageMetadata
+}
+
+const { pageTitle } = Astro.props
+---
+
+
+
+
+
+
+
+
+ {pageTitle &&
{pageTitle} }
+
+
+
+
+
+
+
diff --git a/src/components/Layout.module.css b/src/components/layouts/Base/index.module.css
similarity index 75%
rename from src/components/Layout.module.css
rename to src/components/layouts/Base/index.module.css
index ec6abf4e..a693bb8a 100644
--- a/src/components/Layout.module.css
+++ b/src/components/layouts/Base/index.module.css
@@ -1,7 +1,7 @@
.content {
padding: 0 calc(var(--spacer) / 1.5);
width: 100%;
- max-width: var(--maxWidthContainer);
+ max-width: var(--maxWidthContent);
margin-left: auto;
margin-right: auto;
}
@@ -15,32 +15,28 @@
padding-top: var(--spacer);
background-color: var(--body-background-color);
padding-bottom: calc(var(--spacer) * 2);
- transform: translate3d(0, 0, 0);
- transition: 0.4s var(--easing);
- transition-property: transform, background, border, box-shadow;
border-top: 1px solid rgba(255 255 255 / 85%);
box-shadow:
0 1px 10px rgba(1 85 101 / 10%),
0 -1px 4px rgba(1 85 101 / 5%);
+
+ /* animates the page menu opening/closing */
+ transition: 0.2s ease-out;
+ will-change: transform, background-color;
+ transform: translate3d(0, 0, 0);
}
:global(.has-menu-open) .document {
transform: translate3d(0, calc(var(--spacer) * 2), 0);
}
-:global(.dark) .document {
+[data-theme='dark'] .document {
border-top-color: rgba(255 255 255 / 5%);
box-shadow:
0 1px 8px rgba(0 7 8 / 30%),
0 -1px 4px rgba(0 21 25 / 80%);
}
-@media (min-width: 60rem) {
- .document {
- padding-top: calc(var(--spacer) * 2);
- }
-}
-
@media (min-width: 40rem) and (min-height: 500px) {
.document {
margin-top: calc(var(--spacer) * 2.5);
@@ -53,15 +49,7 @@
}
}
-.container {
- width: 100%;
- max-width: var(--maxWidthContent);
- margin-left: auto;
- margin-right: auto;
-}
-
.wide {
- composes: container;
max-width: none;
}
@@ -70,9 +58,18 @@
margin-right: calc(-50vw + 50%);
}
-@media (min-width: 1000px) {
+@media (min-width: 60rem) {
.breakout {
- margin-left: -8rem;
- margin-right: -8rem;
+ margin-left: -10rem;
+ margin-right: -10rem;
}
}
+
+.pagetitle {
+ font-size: var(--font-size-h3);
+ margin-top: 0;
+ margin-bottom: var(--spacer);
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-end;
+}
diff --git a/src/components/layouts/Post/Action.astro b/src/components/layouts/Post/Action.astro
new file mode 100644
index 00000000..6f7da4d1
--- /dev/null
+++ b/src/components/layouts/Post/Action.astro
@@ -0,0 +1,57 @@
+---
+type Props = {
+ title: string
+ text: string
+ icon: any
+ url?: string
+}
+
+const { title, text, url, icon } = Astro.props
+const Icon = icon
+---
+
+
+
+
+
+ {title}
+ {text}
+
diff --git a/src/components/layouts/Post/Actions.astro b/src/components/layouts/Post/Actions.astro
new file mode 100644
index 00000000..a22a77f0
--- /dev/null
+++ b/src/components/layouts/Post/Actions.astro
@@ -0,0 +1,46 @@
+---
+import config from '@config/blog.config.ts'
+import styles from './Actions.module.css'
+import Action from './Action.astro'
+import { Mastodon, Bitcoin, Github } from '@images/components'
+
+type Props = {
+ githubLink: string
+}
+
+const { githubLink } = Astro.props
+
+const actions = [
+ {
+ title: 'Have a comment?',
+ text: 'Hit me up @krema@mas.to',
+ url: config.author.mastodon,
+ icon: Mastodon
+ },
+ {
+ title: 'Found something useful?',
+ text: 'Say thanks with BTC or ETH',
+ url: '/thanks',
+ icon: Bitcoin
+ },
+ {
+ title: 'Edit on GitHub',
+ text: 'Contribute to this post',
+ url: githubLink,
+ icon: Github
+ }
+]
+---
+
+
+ {
+ actions.map((action) => (
+
+ ))
+ }
+
diff --git a/src/components/layouts/Post/Actions.module.css b/src/components/layouts/Post/Actions.module.css
new file mode 100644
index 00000000..c23f84b2
--- /dev/null
+++ b/src/components/layouts/Post/Actions.module.css
@@ -0,0 +1,16 @@
+.actions {
+ composes: breakout from '../Base/index.module.css';
+ margin-top: calc(var(--spacer) * 2);
+ margin-bottom: calc(var(--spacer) * 2);
+ background: var(--box-background-color);
+ padding: var(--spacer);
+ border-radius: var(--border-radius);
+ border: var(--border-width) solid var(--border-color);
+ display: grid;
+}
+
+@media (min-width: 40rem) {
+ .actions {
+ grid-template-columns: repeat(3, 1fr);
+ }
+}
diff --git a/src/components/layouts/Post/Date.astro b/src/components/layouts/Post/Date.astro
new file mode 100644
index 00000000..b853bab7
--- /dev/null
+++ b/src/components/layouts/Post/Date.astro
@@ -0,0 +1,25 @@
+---
+import Time from '@components/Time.astro'
+
+type Props = {
+ date: Date | undefined
+ updated?: Date
+}
+
+const { date, updated } = Astro.props
+---
+
+
+
+
+
+ {updated && ' • updated '}
+ {updated && }
+
diff --git a/src/components/layouts/Post/LinkActions.astro b/src/components/layouts/Post/LinkActions.astro
new file mode 100644
index 00000000..9ea41edb
--- /dev/null
+++ b/src/components/layouts/Post/LinkActions.astro
@@ -0,0 +1,20 @@
+---
+import styles from './LinkActions.module.css'
+import { ExternalLink, Link } from '@images/components'
+
+type Props = {
+ linkurl?: string
+ slug: string
+}
+
+const { slug, linkurl } = Astro.props
+---
+
+
diff --git a/src/components/templates/Post/LinkActions.module.css b/src/components/layouts/Post/LinkActions.module.css
similarity index 91%
rename from src/components/templates/Post/LinkActions.module.css
rename to src/components/layouts/Post/LinkActions.module.css
index 197174d1..f015b726 100644
--- a/src/components/templates/Post/LinkActions.module.css
+++ b/src/components/layouts/Post/LinkActions.module.css
@@ -1,7 +1,6 @@
.postLinkActions {
display: flex;
justify-content: space-between;
- margin-top: calc(var(--spacer) * 2);
}
.postLinkActions a {
diff --git a/src/components/layouts/Post/Meta.astro b/src/components/layouts/Post/Meta.astro
new file mode 100644
index 00000000..ec525a9b
--- /dev/null
+++ b/src/components/layouts/Post/Meta.astro
@@ -0,0 +1,44 @@
+---
+import type { CollectionEntry } from 'astro:content'
+import slugify from '@lib/slugify'
+import config from '@config/blog.config.ts'
+import Tag from '@components/Tag.astro'
+import styles from './Meta.module.css'
+import Date from './Date.astro'
+
+type Props = {
+ post: CollectionEntry<'articles' | 'links' | 'photos'>
+}
+
+const { collection, data } = Astro.props.post
+const { date, updated, author, tags } = data
+---
+
+
diff --git a/src/components/templates/Post/Meta.module.css b/src/components/layouts/Post/Meta.module.css
similarity index 100%
rename from src/components/templates/Post/Meta.module.css
rename to src/components/layouts/Post/Meta.module.css
diff --git a/src/components/layouts/Post/PrevNext.astro b/src/components/layouts/Post/PrevNext.astro
new file mode 100644
index 00000000..dcc9d04f
--- /dev/null
+++ b/src/components/layouts/Post/PrevNext.astro
@@ -0,0 +1,41 @@
+---
+import styles from './PrevNext.module.css'
+import { ChevronLeft, ChevronRight } from '@images/components'
+
+interface Node {
+ title: string
+ slug: string
+}
+
+type Props = {
+ prev: Node
+ next: Node
+}
+
+const { prev, next } = Astro.props
+---
+
+
+
+
+
diff --git a/src/components/templates/Post/PrevNext.module.css b/src/components/layouts/Post/PrevNext.module.css
similarity index 100%
rename from src/components/templates/Post/PrevNext.module.css
rename to src/components/layouts/Post/PrevNext.module.css
diff --git a/src/components/layouts/Post/Title.astro b/src/components/layouts/Post/Title.astro
new file mode 100644
index 00000000..62be139a
--- /dev/null
+++ b/src/components/layouts/Post/Title.astro
@@ -0,0 +1,36 @@
+---
+import Date from './Date.astro'
+import styles from './Title.module.css'
+import { ExternalLink } from '@images/components'
+
+type Props = {
+ linkurl?: string
+ title: string
+ date?: Date
+ updated?: Date
+ className?: string
+}
+
+const { linkurl, title, date, updated, className } = Astro.props
+const linkHostname = linkurl ? new URL(linkurl).hostname : null
+---
+
+{
+ linkurl ? (
+ <>
+
+ {linkHostname}
+ >
+ ) : (
+ <>
+ {title}
+ {date && }
+ >
+ )
+}
diff --git a/src/components/templates/Post/Title.module.css b/src/components/layouts/Post/Title.module.css
similarity index 85%
rename from src/components/templates/Post/Title.module.css
rename to src/components/layouts/Post/Title.module.css
index 1dfb4e47..57fcbcb2 100644
--- a/src/components/templates/Post/Title.module.css
+++ b/src/components/layouts/Post/Title.module.css
@@ -13,6 +13,7 @@
display: inline-block;
stroke: var(--text-color-light);
margin-bottom: -0.1rem;
+ margin-left: 0.3rem;
}
.linkurl {
@@ -20,10 +21,8 @@
overflow: hidden;
white-space: nowrap;
width: 100%;
- color: var(--text-color);
font-family: var(--font-family-base);
font-size: var(--font-size-small);
- padding: calc(var(--spacer) / 4) 0;
- margin-top: -2rem;
+ color: var(--text-color-light);
margin-bottom: var(--spacer);
}
diff --git a/src/components/layouts/Post/index.astro b/src/components/layouts/Post/index.astro
new file mode 100644
index 00000000..53f26ef7
--- /dev/null
+++ b/src/components/layouts/Post/index.astro
@@ -0,0 +1,74 @@
+---
+import type { CollectionEntry } from 'astro:content'
+import LayoutBase from '@layouts/Base/index.astro'
+import Title from './Title.astro'
+import Actions from './Actions.astro'
+import styles from './index.module.css'
+import Meta from './Meta.astro'
+import Picture from '@components/Picture/index.astro'
+import Toc from '@components/Toc.astro'
+import Exif from '@components/Exif/index.astro'
+import type { Exif as ExifType } from '@lib/exif/types'
+import Changelog from '@components/Changelog/index.astro'
+import RelatedPosts from '@components/RelatedPosts/index.astro'
+import LinkActions from './LinkActions.astro'
+
+type Props = CollectionEntry<'articles' | 'links' | 'photos'> & {
+ lead?: string // comes in through remark plugin as html
+ leadRaw?: string // comes in through remark plugin as plain text
+ tableOfContents?: string // comes in through remark plugin as html
+}
+
+const { data, collection, lead, leadRaw, tableOfContents, slug } = Astro.props
+const { title, date, updated, toc, githubLink, changelog } = data
+const { image, exif } = data as CollectionEntry<'photos'>['data']
+const { linkurl } = data as CollectionEntry<'links'>['data']
+---
+
+
+
+
+
+ {
+ collection === 'articles' && lead ? (
+
+ ) : collection === 'photos' ? (
+
+ ) : null
+ }
+
+ {
+ image && (
+
+ )
+ }
+
+ {collection === 'photos' && exif && }
+
+ {toc && tableOfContents && }
+
+ {collection !== 'photos' && }
+
+ {changelog && }
+
+ {collection === 'links' && }
+
+
+
+ {collection !== 'photos' && }
+
+
+
+ {/* */}
+
+
diff --git a/src/components/layouts/Post/index.module.css b/src/components/layouts/Post/index.module.css
new file mode 100644
index 00000000..1724d1f0
--- /dev/null
+++ b/src/components/layouts/Post/index.module.css
@@ -0,0 +1,66 @@
+.entry {
+ padding-bottom: var(--spacer);
+}
+
+.entry > a {
+ display: block;
+}
+
+.entry p:only-child {
+ margin-bottom: 0;
+}
+
+.entry hr {
+ position: relative;
+ border-bottom: 1px dashed var(--border-color);
+ margin-top: calc(var(--spacer) * var(--line-height));
+ margin-bottom: calc(var(--spacer) * var(--line-height));
+}
+
+.entry hr::before {
+ content: '';
+ position: absolute;
+ left: 0;
+ height: 1px;
+ bottom: -2px;
+ width: 100%;
+ border-bottom: 1px dashed #fff;
+}
+
+:global([data-theme='dark']) .entry hr::before {
+ border-bottom-color: var(--brand-grey);
+}
+
+.entry img {
+ border-radius: var(--border-radius);
+}
+
+.imageWrapper {
+ display: flex;
+ justify-content: center;
+ composes: breakout from '@layouts/Base/index.module.css';
+}
+
+.image {
+ width: fit-content;
+}
+
+.image img:last-child {
+ max-height: 98vh;
+ width: auto;
+}
+
+.lead {
+ font-size: var(--font-size-large);
+ margin-bottom: var(--spacer);
+ margin-top: calc(var(--spacer) / 2);
+}
+
+.lead code {
+ font-size: calc(var(--font-size-large) * 0.9);
+}
+
+/* only Articles have a lead */
+.lead + .imageWrapper {
+ margin-bottom: calc(var(--spacer) * var(--line-height));
+}
diff --git a/src/components/molecules/Menu.tsx b/src/components/molecules/Menu.tsx
deleted file mode 100644
index c38d69f0..00000000
--- a/src/components/molecules/Menu.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import React, { ReactElement, useEffect, useState } from 'react'
-import { Link } from 'gatsby'
-import { useSiteMetadata } from '../../hooks/useSiteMetadata'
-import Hamburger from '../atoms/Hamburger'
-import * as styles from './Menu.module.css'
-
-export default function Menu(): ReactElement {
- const [menuOpen, setMenuOpen] = useState(false)
- const { menu } = useSiteMetadata()
-
- function toggleMenu(): void {
- setMenuOpen(!menuOpen)
- }
-
- useEffect(() => {
- if (menuOpen) {
- document.body.classList.add('has-menu-open')
- } else {
- document.body.classList.remove('has-menu-open')
- }
- }, [menuOpen])
-
- const MenuItems = menu.map((item) => (
-
-
- {item.title}
-
-
- ))
-
- return (
- <>
-
-
-
-
- >
- )
-}
diff --git a/src/components/molecules/Networks.test.tsx b/src/components/molecules/Networks.test.tsx
deleted file mode 100644
index 18b8de77..00000000
--- a/src/components/molecules/Networks.test.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import React from 'react'
-import { render } from '@testing-library/react'
-import meta from '../../../.jest/__fixtures__/meta.json'
-import Networks from './Networks'
-
-const { author, rss, jsonfeed } = meta.site.siteMetadata
-const { twitter, github } = author
-const links = [twitter, github, rss, jsonfeed, 'hello']
-
-describe('Networks', () => {
- it('renders correctly', () => {
- const { container } = render( )
- expect(container.firstChild).toBeInTheDocument()
- })
-})
diff --git a/src/components/molecules/Networks.tsx b/src/components/molecules/Networks.tsx
deleted file mode 100644
index 18a4c349..00000000
--- a/src/components/molecules/Networks.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import React, { ReactElement } from 'react'
-import Icon from '../atoms/Icon'
-import * as styles from './Networks.module.css'
-
-function NetworkIcon({ link }: { link: string }) {
- let IconComp
-
- if (link.includes('mas.to')) {
- IconComp =
- } else if (link.includes('twitter')) {
- IconComp =
- } else if (link.includes('github')) {
- IconComp =
- } else if (link.includes('feed.xml')) {
- IconComp =
- } else if (link.includes('feed.json')) {
- IconComp =
- } else {
- return null
- }
-
- return IconComp
-}
-
-export default function IconLinks({
- links
-}: {
- links: string[]
-}): ReactElement {
- return (
-
- {links.map((link: string) => (
-
-
-
- ))}
-
- )
-}
diff --git a/src/components/molecules/Pagination.tsx b/src/components/molecules/Pagination.tsx
deleted file mode 100644
index ad4a63b1..00000000
--- a/src/components/molecules/Pagination.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import React, { ReactElement } from 'react'
-import { Link } from 'gatsby'
-import { PageContext } from '../../@types/Post'
-import Icon from '../atoms/Icon'
-import * as styles from './Pagination.module.css'
-
-function PageNumber({
- i,
- slug,
- current
-}: {
- i: number
- slug: string
- current?: boolean
-}): JSX.Element {
- const classes = current ? styles.current : styles.number
- const link = i === 0 ? slug : `${slug}page/${i + 1}`
-
- return (
-
- {i + 1}
-
- )
-}
-
-function PrevNext({
- prevPagePath,
- nextPagePath
-}: {
- prevPagePath?: string
- nextPagePath?: string
-}): JSX.Element {
- const link = prevPagePath || nextPagePath
- const rel = prevPagePath ? 'prev' : 'next'
- const title = prevPagePath ? 'Newer Posts' : 'Older Posts'
-
- return (
-
- {prevPagePath ? (
-
- ) : (
-
- )}
-
- )
-}
-
-export default function Pagination({
- pageContext
-}: {
- pageContext: PageContext
-}): ReactElement {
- const { slug, currentPageNumber, numPages, prevPagePath, nextPagePath } =
- pageContext
- const isFirst = currentPageNumber === 1
- const isLast = currentPageNumber === numPages
-
- return (
-
- {!isFirst &&
}
- {Array.from({ length: numPages }, (_, i) => (
-
- ))}
- {!isLast &&
}
-
- )
-}
diff --git a/src/components/molecules/PostDate.module.css b/src/components/molecules/PostDate.module.css
deleted file mode 100644
index a73693ae..00000000
--- a/src/components/molecules/PostDate.module.css
+++ /dev/null
@@ -1,6 +0,0 @@
-.time {
- font-style: italic;
- font-size: var(--font-size-small);
- color: var(--text-color-light);
- margin-bottom: calc(var(--spacer) / var(--line-height));
-}
diff --git a/src/components/molecules/PostDate.tsx b/src/components/molecules/PostDate.tsx
deleted file mode 100644
index 6406302b..00000000
--- a/src/components/molecules/PostDate.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import React, { ReactElement } from 'react'
-import Time from '../atoms/Time'
-import * as styles from './PostDate.module.css'
-
-export default function PostDate({
- date,
- updated
-}: {
- date: string
- updated?: string
-}): ReactElement {
- return (
-
-
- {updated && ' • updated '}
- {updated && }
-
- )
-}
diff --git a/src/components/molecules/PostTeaser.test.tsx b/src/components/molecules/PostTeaser.test.tsx
deleted file mode 100644
index e3d400fc..00000000
--- a/src/components/molecules/PostTeaser.test.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import React from 'react'
-import { render } from '@testing-library/react'
-import post from '../../../.jest/__fixtures__/post.json'
-import PostTeaser from './PostTeaser'
-
-describe('PostTeaser', () => {
- it('renders correctly', () => {
- const { container, rerender } = render(
-
- )
- expect(container.firstChild).toBeInTheDocument()
-
- rerender(
- null}
- />
- )
- })
-})
diff --git a/src/components/molecules/PostTeaser.tsx b/src/components/molecules/PostTeaser.tsx
deleted file mode 100644
index d184898f..00000000
--- a/src/components/molecules/PostTeaser.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import React, { ReactElement } from 'react'
-import { Link, graphql } from 'gatsby'
-import { Image } from '../atoms/Image'
-import PostTitle from '../templates/Post/Title'
-import * as styles from './PostTeaser.module.css'
-
-export const postTeaserQuery = graphql`
- fragment PostTeaser on MarkdownRemark {
- id
- fileAbsolutePath
- frontmatter {
- title
- linkurl
- updated
- image {
- childImageSharp {
- ...ImageFluidThumb
- }
- }
- tags
- }
- fields {
- slug
- date
- type
- }
- }
-`
-
-export default function PostTeaser({
- post,
- toggleSearch,
- hideDate
-}: {
- post: Queries.PostTeaserFragment
- toggleSearch?: () => void
- hideDate?: boolean
-}): ReactElement {
- const { image, title, updated } = post.frontmatter
- const { slug, date } = post.fields
-
- return (
-
- {image ? (
-
- ) : (
-
- )}
-
-
-
- )
-}
diff --git a/src/components/molecules/RelatedPosts.test.tsx b/src/components/molecules/RelatedPosts.test.tsx
deleted file mode 100644
index 5260e426..00000000
--- a/src/components/molecules/RelatedPosts.test.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from 'react'
-import { fireEvent, render, screen } from '@testing-library/react'
-import RelatedPosts from './RelatedPosts'
-
-describe('RelatedPosts', () => {
- it('renders correctly', () => {
- const { container, rerender } = render(
-
- )
- expect(container.firstChild).toBeInTheDocument()
-
- fireEvent.click(screen.getByText('Refresh'))
-
- rerender( )
- })
-})
diff --git a/src/components/molecules/RelatedPosts.tsx b/src/components/molecules/RelatedPosts.tsx
deleted file mode 100644
index f0d00a60..00000000
--- a/src/components/molecules/RelatedPosts.tsx
+++ /dev/null
@@ -1,104 +0,0 @@
-import React, { ReactElement, useState } from 'react'
-import { graphql, useStaticQuery } from 'gatsby'
-import { PhotoThumb } from '../templates/Photos'
-import PostTeaser from './PostTeaser'
-import * as styles from './RelatedPosts.module.css'
-
-const query = graphql`
- query RelatedPosts {
- allArticles: allMarkdownRemark(
- sort: { fields: { date: DESC } }
- filter: { fields: { type: { regex: "/(article|link)/" } } }
- ) {
- edges {
- node {
- ...PostTeaser
- }
- }
- }
-
- allPhotos: allMarkdownRemark(
- sort: { fields: { date: DESC } }
- filter: { fields: { type: { eq: "photo" } } }
- ) {
- edges {
- node {
- ...PostTeaser
- }
- }
- }
- }
-`
-
-function postsWithDataFilter(
- posts:
- | Queries.RelatedPostsQuery['allArticles']['edges']
- | Queries.RelatedPostsQuery['allPhotos']['edges'],
- key: keyof Queries.MarkdownRemarkFrontmatter,
- valuesToFind: string[]
-) {
- let filtered = posts.filter(({ node }) => {
- const frontmatterKey = node.frontmatter[key]
-
- if (
- frontmatterKey !== null &&
- frontmatterKey.some((r: string) => valuesToFind?.includes(r))
- ) {
- return node
- }
- })
-
- if (!filtered?.length) {
- filtered = posts.filter(({ node }) => node)
- }
-
- filtered = filtered.sort(() => 0.5 - Math.random()).slice(0, 6)
-
- return filtered
-}
-
-export default function RelatedPosts({
- tags,
- isPhotos
-}: {
- tags: string[]
- isPhotos?: boolean
-}): ReactElement {
- const data = useStaticQuery(query)
-
- function getPosts() {
- const dataByType = isPhotos ? data.allPhotos.edges : data.allArticles.edges
- const posts = postsWithDataFilter(dataByType, 'tags', tags)
-
- return posts
- }
-
- const [filteredPosts, setFilteredPosts] = useState(getPosts())
-
- function refreshPosts() {
- const newPosts = getPosts()
- setFilteredPosts(newPosts)
- }
-
- return (
-
-
- Related {isPhotos ? 'Photos' : 'Posts'}{' '}
- refreshPosts()}>
- Refresh
-
-
-
- {filteredPosts?.map(({ node }) => (
-
- {isPhotos ? (
-
- ) : (
-
- )}
-
- ))}
-
-
- )
-}
diff --git a/src/components/molecules/Search/SearchButton.module.css b/src/components/molecules/Search/SearchButton.module.css
deleted file mode 100644
index c3e5d227..00000000
--- a/src/components/molecules/Search/SearchButton.module.css
+++ /dev/null
@@ -1,22 +0,0 @@
-.searchButton {
- padding: calc(var(--spacer) / 2);
- vertical-align: middle;
- display: inline-block;
- margin: 0;
- margin-right: calc(var(--spacer) / 4);
-}
-
-.searchButton:focus {
- outline: 0;
-}
-
-.searchButton svg {
- stroke: var(--text-color-light);
- width: 24px;
- height: 24px;
-}
-
-.searchButton:hover svg,
-.searchButton:focus svg {
- stroke: var(--link-color);
-}
diff --git a/src/components/molecules/Search/SearchButton.tsx b/src/components/molecules/Search/SearchButton.tsx
deleted file mode 100644
index 541265f9..00000000
--- a/src/components/molecules/Search/SearchButton.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import React, { ReactElement } from 'react'
-import Icon from '../../atoms/Icon'
-import * as styles from './SearchButton.module.css'
-
-const SearchButton = ({ onClick }: { onClick: () => void }): ReactElement => (
-
-
-
-)
-
-export default SearchButton
diff --git a/src/components/molecules/Search/SearchInput.tsx b/src/components/molecules/Search/SearchInput.tsx
deleted file mode 100644
index 04611f73..00000000
--- a/src/components/molecules/Search/SearchInput.tsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import React, { ChangeEvent, ReactElement } from 'react'
-import Icon from '../../atoms/Icon'
-import Input from '../../atoms/Input'
-import * as styles from './SearchInput.module.css'
-
-export default function SearchInput({
- value,
- onToggle,
- onChange
-}: {
- value: string
- onToggle(): void
- onChange(e: ChangeEvent): void
-}): ReactElement {
- return (
- <>
-
-
-
-
- >
- )
-}
diff --git a/src/components/molecules/Search/SearchResults.tsx b/src/components/molecules/Search/SearchResults.tsx
deleted file mode 100644
index 03106019..00000000
--- a/src/components/molecules/Search/SearchResults.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import React, { ReactElement } from 'react'
-import { graphql, useStaticQuery } from 'gatsby'
-import ReactDOM from 'react-dom'
-import PostTeaser from '../PostTeaser'
-import * as styles from './SearchResults.module.css'
-import SearchResultsEmpty from './SearchResultsEmpty'
-
-export interface Results {
- slug: string
-}
-
-const query = graphql`
- query SearchResults {
- allMarkdownRemark {
- edges {
- node {
- ...PostTeaser
- }
- }
- }
- }
-`
-
-function SearchResultsPure({
- searchQuery,
- results,
- toggleSearch,
- posts
-}: {
- posts: Queries.SearchResultsQuery['allMarkdownRemark']['edges']
- searchQuery: string
- results: Results[]
- toggleSearch(): void
-}) {
- return (
-
- {results.length > 0 ? (
-
- {results.map((page: { slug: string }) =>
- posts
- .filter(({ node }) => node.fields.slug === page.slug)
- .map(({ node }) => (
-
-
-
- ))
- )}
-
- ) : (
-
- )}
-
- )
-}
-
-export default function SearchResults({
- searchQuery,
- results,
- toggleSearch
-}: {
- searchQuery: string
- results: Results[]
- toggleSearch(): void
-}): ReactElement {
- const data = useStaticQuery(query)
- const posts = data.allMarkdownRemark.edges
-
- // creating portal to break out of DOM node we're in
- // and render the results in content container
- return ReactDOM.createPortal(
- ,
- document.getElementById('document')
- )
-}
diff --git a/src/components/molecules/Search/index.module.css b/src/components/molecules/Search/index.module.css
deleted file mode 100644
index f240c060..00000000
--- a/src/components/molecules/Search/index.module.css
+++ /dev/null
@@ -1,46 +0,0 @@
-.search {
- position: absolute;
- left: calc(var(--spacer) / 2);
- right: calc(var(--spacer) / 2);
- top: calc(var(--spacer) / 4);
- z-index: 10;
-}
-
-.search input {
- width: 100%;
-}
-
-@media (min-width: 40rem) {
- .search {
- left: 0;
- right: 0;
- }
-}
-
-.appear,
-.enter {
- opacity: 0.01;
- transform: translate3d(0, -100px, 0);
-}
-
-.appearActive,
-.enterActive {
- opacity: 1;
- transition: 0.2s ease-out;
- transform: translate3d(0, 0, 0);
-}
-
-.exit {
- opacity: 1;
- transform: translate3d(0, 0, 0);
-}
-
-.exitActive {
- opacity: 0.01;
- transition: 0.2s ease-in;
- transform: translate3d(0, -100px, 0);
-}
-
-:global(.hasSearchOpen) {
- overflow: hidden;
-}
diff --git a/src/components/molecules/Search/index.tsx b/src/components/molecules/Search/index.tsx
deleted file mode 100644
index e5fd6cb2..00000000
--- a/src/components/molecules/Search/index.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import React, { ReactElement, useEffect, useState } from 'react'
-import { LazyMotion, domAnimation, m, useReducedMotion } from 'framer-motion'
-import { getAnimationProps, moveInTop } from '../../atoms/Transitions'
-import SearchButton from './SearchButton'
-import SearchInput from './SearchInput'
-import SearchResults from './SearchResults'
-import * as styles from './index.module.css'
-
-export default function Search(): ReactElement {
- const shouldReduceMotion = useReducedMotion()
- const [searchOpen, setSearchOpen] = useState(false)
- const [query, setQuery] = useState('')
- const [results, setResults] = useState([])
-
- useEffect(() => {
- if (!query || !window.__LUNR__) {
- setResults([])
- return
- }
-
- const lunrIndex = window.__LUNR__['en']
- const searchResults = lunrIndex.index.search(query)
- setResults(
- searchResults.map(({ ref }) => {
- return lunrIndex.store[ref]
- })
- )
- }, [query])
-
- useEffect(() => {
- if (searchOpen) {
- document.body.classList.add('hasSearchOpen')
- } else {
- document.body.classList.remove('hasSearchOpen')
- }
- }, [searchOpen])
-
- function toggleSearch(): void {
- setSearchOpen(!searchOpen)
- }
-
- return (
- <>
-
-
- {searchOpen && (
- <>
-
-
- setQuery(e.target.value)}
- onToggle={toggleSearch}
- />
-
-
-
-
- >
- )}
- >
- )
-}
diff --git a/src/components/molecules/ThemeSwitch.test.tsx b/src/components/molecules/ThemeSwitch.test.tsx
deleted file mode 100644
index a4add5d5..00000000
--- a/src/components/molecules/ThemeSwitch.test.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import React from 'react'
-import { fireEvent, render, screen } from '@testing-library/react'
-import ThemeSwitch from './ThemeSwitch'
-
-describe('ThemeSwitch', () => {
- it('renders correctly', async () => {
- render( )
- const element = await screen.findByTitle('Toggle Dark Mode')
- expect(element).toBeInTheDocument()
- })
-
- it('checkbox can be changed', () => {
- const { container } = render( )
-
- const toggle = container.querySelector('input')
- const label = container.querySelector('label')
- fireEvent.click(label)
- fireEvent.change(toggle, { target: { checked: true } })
- })
-})
diff --git a/src/components/molecules/ThemeSwitch.tsx b/src/components/molecules/ThemeSwitch.tsx
deleted file mode 100644
index ebe3bcf5..00000000
--- a/src/components/molecules/ThemeSwitch.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import React, { ReactElement } from 'react'
-import useDarkMode from '../../hooks/useDarkMode'
-import Icon from '../atoms/Icon'
-import * as styles from './ThemeSwitch.module.css'
-
-export default function ThemeSwitch(): ReactElement {
- const { isDarkMode, setIsDarkMode } = useDarkMode()
-
- return (
-
-
setIsDarkMode(!isDarkMode)}
- onKeyPress={() => setIsDarkMode(!isDarkMode)}
- role="presentation"
- >
- Toggle Dark Mode
- setIsDarkMode(!isDarkMode)}
- type="checkbox"
- name="toggle"
- value="toggle"
- aria-describedby="toggle"
- checked={isDarkMode}
- />
-
- {isDarkMode ? : }
-
-
-
- )
-}
diff --git a/src/components/molecules/Vcard.tsx b/src/components/molecules/Vcard.tsx
deleted file mode 100644
index 176205c1..00000000
--- a/src/components/molecules/Vcard.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import React, { ReactElement } from 'react'
-import { graphql, useStaticQuery } from 'gatsby'
-import { getSrc } from 'gatsby-plugin-image'
-import { useSiteMetadata } from '../../hooks/useSiteMetadata'
-import IconLinks from './Networks'
-import * as styles from './Vcard.module.css'
-
-const query = graphql`
- query Avatar {
- avatar: allFile(filter: { name: { eq: "avatar" } }) {
- edges {
- node {
- childImageSharp {
- gatsbyImageData(
- layout: CONSTRAINED
- width: 80
- height: 80
- quality: 85
- )
- }
- }
- }
- }
- }
-`
-
-export default function Vcard(): ReactElement {
- const data = useStaticQuery(query)
- const { author, rss, jsonfeed } = useSiteMetadata()
- const { mastodon, twitter, github, name, uri } = author
- const avatar = getSrc(data.avatar.edges[0].node)
- const links = [mastodon, github, twitter, rss, jsonfeed]
-
- return (
- <>
-
-
- Blog of designer & developer{' '}
-
- {name}
-
-
-
-
- >
- )
-}
diff --git a/src/components/molecules/Web3Donation/InputGroup.test.tsx b/src/components/molecules/Web3Donation/InputGroup.test.tsx
deleted file mode 100644
index 12c81a80..00000000
--- a/src/components/molecules/Web3Donation/InputGroup.test.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import React from 'react'
-import { fireEvent, render } from '@testing-library/react'
-import InputGroup from './InputGroup'
-
-const setAmount = jest.fn()
-
-describe('InputGroup', () => {
- it('renders without crashing', async () => {
- const { container } = render(
-
- )
- expect(container.firstChild).toBeInTheDocument()
-
- const input = container.querySelector('input')
- const button = container.querySelector('button')
- fireEvent.change(input, { target: { value: '3' } })
- fireEvent.click(button)
- expect(setAmount).toHaveBeenCalled()
- })
-})
diff --git a/src/components/organisms/Footer.test.tsx b/src/components/organisms/Footer.test.tsx
deleted file mode 100644
index cced6745..00000000
--- a/src/components/organisms/Footer.test.tsx
+++ /dev/null
@@ -1,7 +0,0 @@
-import React from 'react'
-import testRender from '../../../.jest/testRender'
-import Footer from './Footer'
-
-describe('Footer', () => {
- testRender()
-})
diff --git a/src/components/organisms/Footer.tsx b/src/components/organisms/Footer.tsx
deleted file mode 100644
index 1cdd5969..00000000
--- a/src/components/organisms/Footer.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import React from 'react'
-import { Link } from 'gatsby'
-import { useSiteMetadata } from '../../hooks/useSiteMetadata'
-import Icon from '../atoms/Icon'
-import Vcard from '../molecules/Vcard'
-import * as styles from './Footer.module.css'
-
-function Copyright() {
- const { name, uri, github } = useSiteMetadata().author
- const year = new Date().getFullYear()
-
- return (
-
- )
-}
-
-export default function Footer(): JSX.Element {
- return (
-
- )
-}
diff --git a/src/components/organisms/Header.test.tsx b/src/components/organisms/Header.test.tsx
deleted file mode 100644
index 066aa1dd..00000000
--- a/src/components/organisms/Header.test.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import React from 'react'
-import { fireEvent, render, screen } from '@testing-library/react'
-import Header from './Header'
-
-describe('Header', () => {
- it('renders correctly', () => {
- const { container } = render(
-
-
-
- )
- expect(container.firstChild).toBeInTheDocument()
- fireEvent.click(screen.getByTitle('Menu'))
- fireEvent.click(screen.getByTitle('Search'))
-
- const input = screen.getByPlaceholderText('Search everything')
- fireEvent.change(input, { target: { value: 'wallpaper' } })
- })
-})
diff --git a/src/components/organisms/Header.tsx b/src/components/organisms/Header.tsx
deleted file mode 100644
index c5e9c24f..00000000
--- a/src/components/organisms/Header.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import React from 'react'
-import { Link } from 'gatsby'
-import { ReactComponent as Logo } from '../../images/logo.svg'
-import Menu from '../molecules/Menu'
-import Search from '../molecules/Search'
-import ThemeSwitch from '../molecules/ThemeSwitch'
-import * as styles from './Header.module.css'
-
-export default function Header(): JSX.Element {
- return (
-
- )
-}
diff --git a/src/components/templates/Archive.module.css b/src/components/templates/Archive.module.css
deleted file mode 100644
index 684e74ff..00000000
--- a/src/components/templates/Archive.module.css
+++ /dev/null
@@ -1,14 +0,0 @@
-.posts {
- display: grid;
- gap: calc(var(--spacer) * 2);
-}
-
-@media (min-width: 40rem) {
- .posts {
- grid-template-columns: repeat(2, 1fr);
- }
-}
-
-.posts h1 {
- font-size: var(--font-size-h4);
-}
diff --git a/src/components/templates/Archive.test.tsx b/src/components/templates/Archive.test.tsx
deleted file mode 100644
index c605f7b0..00000000
--- a/src/components/templates/Archive.test.tsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import React from 'react'
-import { render } from '@testing-library/react'
-import data from '../../../.jest/__fixtures__/posts.json'
-import Archive from './Archive'
-
-describe('Archive', () => {
- const pageContext = {
- tag: 'hello',
- slug: '/hello',
- currentPageNumber: 2,
- numPages: 20
- }
-
- it('renders without crashing', () => {
- const { container } = render(
-
- )
- expect(container.firstChild).toBeInTheDocument()
- })
-})
diff --git a/src/components/templates/Archive.tsx b/src/components/templates/Archive.tsx
deleted file mode 100644
index 18f1cb57..00000000
--- a/src/components/templates/Archive.tsx
+++ /dev/null
@@ -1,71 +0,0 @@
-import React, { ReactElement } from 'react'
-import { graphql } from 'gatsby'
-import { PageContext } from '../../@types/Post'
-import HeadMeta, { HeadMetaProps } from '../atoms/HeadMeta'
-import Pagination from '../molecules/Pagination'
-import PostTeaser from '../molecules/PostTeaser'
-import * as styles from './Archive.module.css'
-import Page from './Page'
-
-function getMetadata(pageContext: PageContext) {
- const { tag, currentPageNumber, numPages } = pageContext
- const title = tag ? `#${tag}` : 'Archive'
- const paginationTitle =
- numPages > 1 && currentPageNumber > 1
- ? `Page ${currentPageNumber} / ${numPages}`
- : ''
-
- const meta: Partial = {
- title: `${title} ${paginationTitle}`,
- description: 'All the articles & links.'
- }
-
- return meta
-}
-
-export default function Archive({
- data,
- pageContext
-}: {
- data: Queries.ArchiveTemplateQuery
- pageContext: PageContext
-}): ReactElement {
- const edges = data.allMarkdownRemark.edges
- const meta = getMetadata(pageContext)
-
- const PostsList = edges.map(({ node }) => (
-
- ))
-
- return (
-
- {PostsList}
- {pageContext.numPages > 1 && }
-
- )
-}
-
-export function Head({ pageContext }: { pageContext: PageContext }) {
- const meta = getMetadata(pageContext)
- return
-}
-
-export const archiveQuery = graphql`
- query ArchiveTemplate($tag: String, $skip: Int, $limit: Int) {
- allMarkdownRemark(
- filter: {
- fields: { type: { nin: "photo" } }
- frontmatter: { tags: { eq: $tag } }
- }
- sort: { fields: { date: DESC } }
- skip: $skip
- limit: $limit
- ) {
- edges {
- node {
- ...PostTeaser
- }
- }
- }
- }
-`
diff --git a/src/components/templates/Page.module.css b/src/components/templates/Page.module.css
deleted file mode 100644
index a0a8e6c6..00000000
--- a/src/components/templates/Page.module.css
+++ /dev/null
@@ -1,9 +0,0 @@
-.pagetitle {
- font-size: var(--font-size-h3);
- margin-top: 0;
- margin-bottom: var(--spacer);
- color: var(--text-color-light);
- display: flex;
- justify-content: space-between;
- align-items: flex-end;
-}
diff --git a/src/components/templates/Page.tsx b/src/components/templates/Page.tsx
deleted file mode 100644
index 49386a5b..00000000
--- a/src/components/templates/Page.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import React, { ReactElement, ReactNode } from 'react'
-import * as styles from './Page.module.css'
-
-export default function Page({
- title,
- section,
- children
-}: {
- title: string
- children: ReactNode
- section?: string
-}): ReactElement {
- return (
- <>
- {title}
- {section ? : children}
- >
- )
-}
diff --git a/src/components/templates/Photos.module.css b/src/components/templates/Photos.module.css
deleted file mode 100644
index 8325dd5f..00000000
--- a/src/components/templates/Photos.module.css
+++ /dev/null
@@ -1,26 +0,0 @@
-.photos {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: var(--spacer);
-}
-
-@media (min-width: 40rem) {
- .photos {
- grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
- }
-}
-
-.photo a {
- display: block;
-}
-
-.photo figure,
-.photo figure > div {
- margin: 0;
-}
-
-.photo figcaption {
- font-size: var(--font-size-base);
- padding-left: calc(var(--spacer) / 2);
- padding-right: calc(var(--spacer) / 2);
-}
diff --git a/src/components/templates/Photos.test.tsx b/src/components/templates/Photos.test.tsx
deleted file mode 100644
index f8343612..00000000
--- a/src/components/templates/Photos.test.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import React from 'react'
-import { render } from '@testing-library/react'
-import data from '../../../.jest/__fixtures__/photos.json'
-import Photos from './Photos'
-
-describe('/photos', () => {
- it('renders without crashing', () => {
- const pageContext = {
- slug: '/photos',
- currentPageNumber: 2,
- numPages: 20
- }
-
- const { container } = render(
- // @ts-expect-error: only testing first render
-
- )
- expect(container.firstChild).toBeInTheDocument()
- })
-})
diff --git a/src/components/templates/Photos.tsx b/src/components/templates/Photos.tsx
deleted file mode 100644
index f0c09e30..00000000
--- a/src/components/templates/Photos.tsx
+++ /dev/null
@@ -1,107 +0,0 @@
-import React, { ReactElement } from 'react'
-import { Link, PageProps, graphql } from 'gatsby'
-import { PageContext } from '../../@types/Post'
-import HeadMeta, { HeadMetaProps } from '../atoms/HeadMeta'
-import { Image } from '../atoms/Image'
-import Pagination from '../molecules/Pagination'
-import Page from './Page'
-import * as styles from './Photos.module.css'
-
-export const PhotoThumb = ({
- photo
-}: {
- photo: Queries.PhotosTemplateQuery['allMarkdownRemark']['edges'][0]['node']
-}): ReactElement => {
- const { title, image } = photo.frontmatter
- const { slug } = photo.fields
- const { gatsbyImageData } = (image as any).childImageSharp
-
- return (
-
- {image && (
-
-
-
- )}
-
- )
-}
-
-interface PhotosPageProps extends PageProps {
- data: Queries.PhotosTemplateQuery
- pageContext: PageContext
-}
-
-function getMetadata(currentPageNumber: number, numPages: number) {
- const paginationTitle =
- numPages > 1 && currentPageNumber > 1
- ? `Page ${currentPageNumber} / ${numPages}`
- : ''
-
- const meta: Partial = {
- title: `Photos ${paginationTitle}`,
- description: 'Personal photos of designer & developer Matthias Kretschmann.'
- }
-
- return meta
-}
-
-export default function Photos({
- data,
- pageContext
-}: PhotosPageProps): ReactElement {
- const photos = data.allMarkdownRemark.edges
- const { currentPageNumber, numPages } = pageContext
- const meta = getMetadata(currentPageNumber, numPages)
-
- return (
-
-
- {photos.map(({ node }) => (
-
- ))}
-
-
- {numPages > 1 && }
-
- )
-}
-
-export function Head({
- pageContext
-}: {
- pageContext: PhotosPageProps['pageContext']
-}) {
- const { currentPageNumber, numPages } = pageContext
- const meta = getMetadata(currentPageNumber, numPages)
- return
-}
-
-export const photosQuery = graphql`
- query PhotosTemplate($skip: Int, $limit: Int) {
- allMarkdownRemark(
- filter: { fields: { type: { eq: "photo" } } }
- sort: { fields: { date: DESC } }
- skip: $skip
- limit: $limit
- ) {
- edges {
- node {
- id
- frontmatter {
- title
- image {
- childImageSharp {
- ...PhotoFluidThumb
- }
- }
- }
- fields {
- slug
- type
- }
- }
- }
- }
- }
-`
diff --git a/src/components/templates/Post/Actions.module.css b/src/components/templates/Post/Actions.module.css
deleted file mode 100644
index 6fe093f3..00000000
--- a/src/components/templates/Post/Actions.module.css
+++ /dev/null
@@ -1,57 +0,0 @@
-.actions {
- composes: breakout from '../../Layout.module.css';
- margin-top: calc(var(--spacer) * 2);
- margin-bottom: calc(var(--spacer) * 2);
- background: var(--box-background-color);
- padding: var(--spacer);
- border-radius: var(--border-radius);
- display: grid;
-}
-
-@media (min-width: 40rem) {
- .actions {
- grid-template-columns: repeat(3, 1fr);
- }
-}
-
-.link {
- transition: 0.2s ease-out;
- color: var(--link-color);
-}
-
-.actionTitle {
- font-size: var(--font-size-base);
- color: var(--text-color);
- margin: 0;
- transition: 0.2s ease-out;
-}
-
-.actionText {
- font-size: var(--font-size-small);
- color: var(--text-color-light);
- margin-bottom: 0;
- transition: color 0.2s ease-out;
-}
-
-.action {
- display: block;
- margin: 0;
- padding: var(--spacer) calc(var(--spacer) / 2) var(--spacer)
- calc(var(--spacer) * 1.5);
- position: relative;
- text-align: left;
-}
-
-.action:hover,
-.action:focus {
- cursor: pointer;
-}
-
-.action svg {
- width: 20px;
- height: 20px;
- position: absolute;
- left: calc(var(--spacer) / 1.5);
- top: calc(var(--spacer) / 1.05);
- stroke: var(--text-color-light);
-}
diff --git a/src/components/templates/Post/Actions.tsx b/src/components/templates/Post/Actions.tsx
deleted file mode 100644
index 3e126afb..00000000
--- a/src/components/templates/Post/Actions.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import React, { ReactElement } from 'react'
-import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
-import Icon from '../../atoms/Icon'
-import * as styles from './Actions.module.css'
-
-interface ActionProps {
- title: string
- text: string
- icon: string
- url?: string
- onClick?(): void
-}
-
-const Action = ({ title, text, url, icon, onClick }: ActionProps) => {
- return (
-
-
- {title}
- {text}
-
- )
-}
-
-export default function PostActions({
- githubLink
-}: {
- githubLink: string
-}): ReactElement {
- const { author } = useSiteMetadata()
-
- return (
-
- )
-}
diff --git a/src/components/templates/Post/Content.module.css b/src/components/templates/Post/Content.module.css
deleted file mode 100644
index 1642aab2..00000000
--- a/src/components/templates/Post/Content.module.css
+++ /dev/null
@@ -1,11 +0,0 @@
-.content hr {
- /* composes: divider from '../../atoms/Divider.module.css'; */
-
- margin-top: calc(var(--spacer) * var(--line-height));
- margin-bottom: calc(var(--spacer) * var(--line-height));
- border-bottom: 1px dashed var(--border-color);
-}
-
-.content img {
- border-radius: var(--border-radius);
-}
diff --git a/src/components/templates/Post/Content.tsx b/src/components/templates/Post/Content.tsx
deleted file mode 100644
index e3325b87..00000000
--- a/src/components/templates/Post/Content.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import React, { ReactElement } from 'react'
-import Changelog from '../../atoms/Changelog'
-import * as styles from './Content.module.css'
-import PostToc from './Toc'
-
-export default function PostContent({
- post
-}: {
- post: Queries.BlogPostBySlugQuery['post']
-}): ReactElement {
- const separator = ''
- const changelog = post.frontmatter.changelog
-
- let content = post.html
-
- if (post.fields.type === 'article') {
- // Remove lead paragraph from content
- if (content.includes(separator)) {
- content = content.split(separator)[1]
- } else {
- const lead = content.split('\n')[0]
- content = content.replace(lead, '')
- }
- }
-
- return (
- <>
- {post.frontmatter.toc && (
-
- )}
-
- {changelog && }
- >
- )
-}
diff --git a/src/components/templates/Post/Lead.module.css b/src/components/templates/Post/Lead.module.css
deleted file mode 100644
index 7c80f32d..00000000
--- a/src/components/templates/Post/Lead.module.css
+++ /dev/null
@@ -1,9 +0,0 @@
-.lead {
- font-size: var(--font-size-large);
- margin-bottom: calc(var(--spacer) * var(--line-height));
-}
-
-.index {
- font-size: var(--font-size-base);
- margin-top: calc(var(--spacer) * var(--line-height));
-}
diff --git a/src/components/templates/Post/Lead.tsx b/src/components/templates/Post/Lead.tsx
deleted file mode 100644
index ecb0270c..00000000
--- a/src/components/templates/Post/Lead.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import React, { ReactElement } from 'react'
-import * as styles from './Lead.module.css'
-
-// Extract lead paragraph from content
-// Grab everything before more tag, or just first paragraph
-const PostLead = ({
- post,
- className
-}: {
- post: Queries.BlogPostBySlugQuery['post']
- className?: string
-}): ReactElement => {
- let lead
- const content = post.html
- const separator = ''
-
- if (content.includes(separator)) {
- lead = content.split(separator)[0]
- } else {
- lead = content.split('\n')[0]
- }
-
- return (
-
- )
-}
-
-export default PostLead
diff --git a/src/components/templates/Post/LinkActions.tsx b/src/components/templates/Post/LinkActions.tsx
deleted file mode 100644
index d13de056..00000000
--- a/src/components/templates/Post/LinkActions.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import React, { ReactElement } from 'react'
-import { Link } from 'gatsby'
-import Icon from '../../atoms/Icon'
-import * as styles from './LinkActions.module.css'
-import * as stylesMore from './More.module.css'
-
-const PostLinkActions = ({
- linkurl,
- slug
-}: {
- linkurl?: string
- slug: string
-}): ReactElement => (
-
-)
-
-export default PostLinkActions
diff --git a/src/components/templates/Post/Meta.tsx b/src/components/templates/Post/Meta.tsx
deleted file mode 100644
index 385357d0..00000000
--- a/src/components/templates/Post/Meta.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import React, { ReactElement } from 'react'
-import { Link } from 'gatsby'
-import slugify from 'slugify'
-import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
-import Tag from '../../atoms/Tag'
-import PostDate from '../../molecules/PostDate'
-import * as styles from './Meta.module.css'
-
-export default function PostMeta({
- post
-}: {
- post: Queries.BlogPostBySlugQuery['post']
-}): ReactElement {
- const siteMeta = useSiteMetadata()
- const { author, updated, tags } = post.frontmatter
- const { date, type } = post.fields
-
- return (
-
- )
-}
diff --git a/src/components/templates/Post/More.module.css b/src/components/templates/Post/More.module.css
deleted file mode 100644
index 5c2f764f..00000000
--- a/src/components/templates/Post/More.module.css
+++ /dev/null
@@ -1,25 +0,0 @@
-.postMore {
- display: inline-block;
- font-family: var(--font-family-headings);
- font-weight: var(--font-weight-headings);
- font-size: calc(var(--font-size-base) * 0.9);
- color: var(--link-color);
- text-transform: uppercase;
- margin-top: var(--spacer);
-}
-
-.postMore svg {
- display: inline-block;
- margin: 0;
- top: 0.25rem;
- position: relative;
- stroke: var(--text-color-light);
- transition: 0.2s ease-out;
- width: 20px;
- height: 20px;
-}
-
-.postMore:hover svg,
-.postMore:focus svg {
- transform: translate3d(0.2rem, 0, 0);
-}
diff --git a/src/components/templates/Post/More.tsx b/src/components/templates/Post/More.tsx
deleted file mode 100644
index cb8251b9..00000000
--- a/src/components/templates/Post/More.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import React, { ReactElement } from 'react'
-import { Link } from 'gatsby'
-import Icon from '../../atoms/Icon'
-import * as styles from './More.module.css'
-
-const PostMore = ({
- to,
- children
-}: {
- to: string
- children: string
-}): ReactElement => (
-
- {children}
-
-
-)
-
-export default PostMore
diff --git a/src/components/templates/Post/PrevNext.tsx b/src/components/templates/Post/PrevNext.tsx
deleted file mode 100644
index 89d6aec3..00000000
--- a/src/components/templates/Post/PrevNext.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import React, { ReactElement } from 'react'
-import { Link } from 'gatsby'
-import Icon from '../../atoms/Icon'
-import * as styles from './PrevNext.module.css'
-
-interface Node {
- title: string
- slug: string
-}
-
-interface PrevNextProps {
- prev: Node
- next: Node
-}
-
-const PrevNext = ({ prev, next }: PrevNextProps): ReactElement => (
-
-
- {prev && (
-
-
-
Newer
-
{prev.title}
-
- )}
-
-
- {next && (
-
-
Older
-
{next.title}
-
-
- )}
-
-
-)
-
-export default PrevNext
diff --git a/src/components/templates/Post/Title.tsx b/src/components/templates/Post/Title.tsx
deleted file mode 100644
index 83c02bab..00000000
--- a/src/components/templates/Post/Title.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-import React, { ReactElement } from 'react'
-import Icon from '../../atoms/Icon'
-import PostDate from '../../molecules/PostDate'
-import * as styles from './Title.module.css'
-
-export default function PostTitle({
- linkurl,
- title,
- date,
- updated,
- className
-}: {
- linkurl?: string
- title: string
- date?: string
- updated?: string
- className?: string
-}): ReactElement {
- const linkHostname = linkurl ? new URL(linkurl).hostname : null
-
- return linkurl ? (
- <>
-
- {linkHostname}
- >
- ) : (
- <>
- {title}
- {date && }
- >
- )
-}
diff --git a/src/components/templates/Post/Toc.module.css b/src/components/templates/Post/Toc.module.css
deleted file mode 100644
index 4a5c1b95..00000000
--- a/src/components/templates/Post/Toc.module.css
+++ /dev/null
@@ -1,15 +0,0 @@
-.toc {
- background: var(--box-background-color);
- padding: var(--spacer);
- border-radius: var(--border-radius);
- margin-bottom: calc(var(--spacer) * var(--line-height));
-}
-
-.toc ul {
- margin: 0;
-}
-
-.toc li {
- margin-top: calc(var(--spacer) / 6);
- margin-bottom: calc(var(--spacer) / 6);
-}
diff --git a/src/components/templates/Post/Toc.tsx b/src/components/templates/Post/Toc.tsx
deleted file mode 100644
index e4ddf784..00000000
--- a/src/components/templates/Post/Toc.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import React, { ReactElement } from 'react'
-import * as styles from './Toc.module.css'
-
-const PostToc = ({
- tableOfContents
-}: {
- tableOfContents: string
-}): ReactElement => {
- return (
-
- )
-}
-
-export default PostToc
diff --git a/src/components/templates/Post/index.module.css b/src/components/templates/Post/index.module.css
deleted file mode 100644
index 9d30a85f..00000000
--- a/src/components/templates/Post/index.module.css
+++ /dev/null
@@ -1,17 +0,0 @@
-.hentry {
- composes: container from '../../Layout.module.css';
- width: 100%;
- padding-bottom: var(--spacer);
-}
-
-.hentry > a {
- display: block;
-}
-
-.hentry:only-child {
- padding-bottom: var(--spacer);
-}
-
-.image {
- composes: breakout from '../../Layout.module.css';
-}
diff --git a/src/components/templates/Post/index.test.tsx b/src/components/templates/Post/index.test.tsx
deleted file mode 100644
index 38735cec..00000000
--- a/src/components/templates/Post/index.test.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import React from 'react'
-import { render } from '@testing-library/react'
-import Post from '.'
-import link from '../../../../.jest/__fixtures__/link.json'
-import post from '../../../../.jest/__fixtures__/post.json'
-import postWithMore from '../../../../.jest/__fixtures__/postWithMore.json'
-
-describe('Post', () => {
- const pageContext = {
- next: { title: 'hello', slug: '/hello' },
- prev: { title: 'hello2', slug: '/hello2' }
- }
-
- it('renders without crashing', () => {
- const { container, rerender } = render(
-
- )
- expect(container.firstChild).toBeInTheDocument()
-
- rerender(
-
- )
- rerender( )
- })
-})
diff --git a/src/components/templates/Post/index.tsx b/src/components/templates/Post/index.tsx
deleted file mode 100644
index 3bc6d606..00000000
--- a/src/components/templates/Post/index.tsx
+++ /dev/null
@@ -1,161 +0,0 @@
-import React, { ReactElement } from 'react'
-import { graphql } from 'gatsby'
-import { PageContext } from '../../../@types/Post'
-import { useSiteMetadata } from '../../../hooks/useSiteMetadata'
-import Exif from '../../atoms/Exif'
-import HeadMeta from '../../atoms/HeadMeta'
-import SchemaOrg from '../../atoms/HeadMeta/SchemaOrg'
-import { Image } from '../../atoms/Image'
-import RelatedPosts from '../../molecules/RelatedPosts'
-import PostActions from './Actions'
-import PostContent from './Content'
-import PostLead from './Lead'
-import PostLinkActions from './LinkActions'
-import PostMeta from './Meta'
-import PrevNext from './PrevNext'
-import PostTitle from './Title'
-import * as styles from './index.module.css'
-
-export default function Post({
- data,
- pageContext: { next, prev }
-}: {
- data: Queries.BlogPostBySlugQuery
- pageContext: {
- next: { title: string; slug: string }
- prev: { title: string; slug: string }
- }
-}): ReactElement {
- const { post } = data
- const { title, image, linkurl, tags, updated } = post.frontmatter
- const { slug, githubLink, date, type } = post.fields
-
- return (
- <>
-
-
-
- {type === 'article' && }
- {type === 'photo' && }
-
- {image && (
-
- )}
-
- {type === 'photo' ? (
- image?.fields && (
-
- )
- ) : (
-
- )}
-
- {type === 'link' && }
-
- {type !== 'photo' && }
-
-
-
-
- >
- )
-}
-
-export function Head({
- pageContext,
- data
-}: {
- pageContext: PageContext
- data: Queries.BlogPostBySlugQuery
-}): ReactElement {
- const { siteUrl } = useSiteMetadata()
- const { excerpt, rawMarkdownBody } = data.post
- const { title, image, style, updated } = data.post.frontmatter
- const { date } = data.post.fields
- const description = excerpt || rawMarkdownBody
-
- return (
-
- <>
-
- {style && }
- >
-
- )
-}
-
-export const pageQuery = graphql`
- query BlogPostBySlug($slug: String!) {
- post: markdownRemark(fields: { slug: { eq: $slug } }) {
- html
- excerpt
- frontmatter {
- title
- image {
- childImageSharp {
- ...ImageFluid
- }
- fields {
- exif {
- formatted {
- iso
- model
- fstop
- shutterspeed
- focalLength
- lensModel
- exposure
- gps {
- latitude
- longitude
- }
- }
- }
- }
- }
- toc
- author
- updated
- tags
- linkurl
- style {
- publicURL
- }
- changelog
- }
- fields {
- type
- slug
- date
- githubLink
- }
- rawMarkdownBody
- tableOfContents(maxDepth: 3)
- }
- }
-`
diff --git a/src/env.d.ts b/src/env.d.ts
new file mode 100644
index 00000000..6811d7c2
--- /dev/null
+++ b/src/env.d.ts
@@ -0,0 +1,3 @@
+// eslint-disable-next-line @typescript-eslint/triple-slash-reference
+///
+///
diff --git a/src/helpers/umami.ts b/src/helpers/umami.ts
deleted file mode 100644
index 4c689f1c..00000000
--- a/src/helpers/umami.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-const UMAMI_SCRIPT_URL = process.env.GATSBY_UMAMI_SCRIPT_URL
-const UMAMI_WEBSITE_ID = process.env.GATSBY_UMAMI_WEBSITE_ID
-const isProduction = process.env.NODE_ENV === 'production'
-
-if (isProduction && (!UMAMI_SCRIPT_URL || !UMAMI_WEBSITE_ID)) {
- throw new Error('Missing Umami environment variables')
-}
-
-export { UMAMI_SCRIPT_URL, UMAMI_WEBSITE_ID }
diff --git a/src/helpers/wrapPageElement.test.tsx b/src/helpers/wrapPageElement.test.tsx
deleted file mode 100644
index 7467c06c..00000000
--- a/src/helpers/wrapPageElement.test.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import React from 'react'
-import { render } from '@testing-library/react'
-import WrapPageElement from './wrapPageElement'
-
-describe('wrapPageElement', () => {
- it('renders correctly', () => {
- const { container } = render(
- // @ts-expect-error: only testing first render
-
- )
- expect(container.firstChild).toBeInTheDocument()
- })
-})
diff --git a/src/helpers/wrapPageElement.tsx b/src/helpers/wrapPageElement.tsx
deleted file mode 100644
index b417f420..00000000
--- a/src/helpers/wrapPageElement.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import React, { ReactElement } from 'react'
-import type { GatsbyBrowser, GatsbySSR } from 'gatsby'
-import Layout from '../components/Layout'
-
-const wrapPageElement:
- | GatsbyBrowser['wrapPageElement']
- | GatsbySSR['wrapPageElement'] = ({ element, props }): ReactElement => (
- {element}
-)
-
-export default wrapPageElement
diff --git a/src/hooks/useDarkMode.ts b/src/hooks/useDarkMode.ts
deleted file mode 100644
index d49e0074..00000000
--- a/src/hooks/useDarkMode.ts
+++ /dev/null
@@ -1,84 +0,0 @@
-//
-// adapted from
-// https://github.com/daveschumaker/react-dark-mode-hook/blob/master/useDarkMode.js
-//
-import {
- Dispatch,
- SetStateAction,
- useCallback,
- useEffect,
- useState
-} from 'react'
-
-const isClient = typeof window === 'object'
-
-function getDarkMode() {
- // if (localStorage.getItem('theme') === 'dark') {
- // return true
- // } else if (localStorage.getItem('theme') === 'light') {
- // return false
- // }
-
- if (
- isClient &&
- window.matchMedia &&
- window.matchMedia('(prefers-color-scheme: dark)').matches
- ) {
- return true
- } else {
- return false
- }
-}
-
-export type UseDarkMode = {
- isDarkMode: boolean
- themeColor: string
- setIsDarkMode: Dispatch>
-}
-
-export default function useDarkMode(): UseDarkMode {
- const [isDarkMode, setIsDarkMode] = useState(getDarkMode())
- const [themeColor, setThemeColor] = useState()
-
- const changeTheme = useCallback(() => {
- if (isDarkMode) {
- document.documentElement.classList.add('dark')
- } else {
- document.documentElement.classList.remove('dark')
- }
- setThemeColor(isDarkMode === true ? '#1d2224' : '#e7eef4')
- }, [isDarkMode])
-
- //
- // Init
- //
- useEffect(() => {
- changeTheme()
- }, [changeTheme])
-
- //
- // Handle system theme change events
- //
- const handleChange = useCallback(() => {
- setIsDarkMode(getDarkMode())
- }, [])
-
- useEffect(() => {
- if (!isClient) return
-
- const darkModeQuery = window.matchMedia('(prefers-color-scheme: dark)')
-
- try {
- darkModeQuery.addEventListener('change', handleChange)
- } catch (addEventListenerError) {
- console.error(addEventListenerError)
- }
-
- return () =>
- window
- .matchMedia('(prefers-color-scheme: dark)')
- .removeEventListener('change', handleChange)
- }, [handleChange])
-
- return { isDarkMode, setIsDarkMode, themeColor }
-}
diff --git a/src/hooks/useSiteMetadata.ts b/src/hooks/useSiteMetadata.ts
deleted file mode 100644
index 32c59df0..00000000
--- a/src/hooks/useSiteMetadata.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import { graphql, useStaticQuery } from 'gatsby'
-
-const query = graphql`
- query SiteMetadata {
- site {
- siteMetadata {
- siteTitle
- siteTitleShort
- siteDescription
- siteUrl
- author {
- name
- email
- uri
- twitter
- mastodon
- github
- bitcoin
- ether
- }
- menu {
- title
- link
- }
- rss
- jsonfeed
- itemsPerPage
- repoContentPath
- }
- }
- }
-`
-
-export function useSiteMetadata() {
- const { site } = useStaticQuery(query)
- return site.siteMetadata
-}
diff --git a/src/images/apple-touch-icon.png b/src/images/apple-touch-icon.png
deleted file mode 100644
index 6b6b74f269799cbdc36f978da119caf436913922..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 10894
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
C>Ctxp
diff --git a/src/images/favicon-16x16.png b/src/images/favicon-16x16.png
deleted file mode 100644
index 262dccea9607740887d5f3e5e6cc217eba5012ca..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 819
zcmX9*doa{d6#mv@5^Zf%q!KYkno#{?nx;lWs2J}zt!GSR*+tfCZLcMd^;qoMf|oVj@(?#^^wV_g7rR~JVwf(&)&
zXp(HTf0H)>t+Nhp4uG;)?{~O1;ii!;UTy%_65w3|ei4Z`3#8Zqb4=iH6=2Acc6lBF
zXw@F4eVoVu{8(P@KiWiR
zW*|C8D=M1|ASM@am)BZ7Zzt&~B^y$NTA32n#FU9hVIflR2Pf9L`J*#C6hpL|FG2OL
zDQg)a90ak|rU5K4(-EG5hzu~fIFpXhR77Nf7{%}qdl9jGwOw=$qH=JSPkfP(2i7$t
z7a^$tiTQ}XigN<63c(Q~tqj~sWJ-`FMn)wrh>%spEQL03FCxq?DZ-KA_qE3kKLed4<15n+l?S||=B==Bp2W3hW
zwIQz=RVq+DXzWE(KboJQ^(k5gaHAiMeW>Y%e3;fTLQ}q`b&f*$3W^c552N)3ng>ze
zPrgMXZn~X-Eyv5<8yYDeR>se3iVBX6jdK=@bf>#w#V1#O&U&ogy`_5B_ec68x5v|I
zba{2enNo?37FgrtDJ|lZvm*rByIcra&vFT519}GPpg|%vB4U@Tql1spt4OA)zTLqC
zw-lX9b1nO&W%`Cfd$XN3=S{*Q4ZIJr^^RuKggi>sD7sVFE_)~xD|CdmbCa{57N+Lr
zK7R{0x3qa|x_9R5%;NO?m)#7jEf)LD2TctZ4D^k+nQY#^b(aBS-g<~)7}g4_%B7`y
z%B{DUSu_k#004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006
zVoOIv00000008+zyMF)x010qNS#tmY3ljhU3ljkVnw%H_00gN?L_t(o!|jz_h*eb-
z$A4>|ea^Xe?l?|<;6M>mNKpBxfskWCCNvY7V?;`$l&B-pWY|l+Xpw{vBMfLWBBLXU
zS@}`fX!9^c)&v$JjOf!)JxQbGeBY0A_U>Vyd+%tOsq|8_V6)G;mvh$o|JK_715+^-
z|M!qPe)5G)_oBRwAkSjrxmXim%rBVu6vlS)*`BliUIC38n?&nfR_P4ZfQcbC5JzAQ
z#KvNyPK<4#yKD6CUZrk3`LgvJK<%@lGI$;+?|K~;utv0aO&)TZr&oD*MqtUx86eLf
zQjN&BoIY@lhP4l?z&~%a_R#@^X^8VsC_r3*D26z~7fjo_mFM2-{OF;pM|M|>h`GH2
zmaKe2M2=agMg)`srFK_Mn|A+jA*v?~AxycI&p{z_g+!5SLu6b*u>oUTTcDb+4h`M9
z1xsI^uKnP+6;+L=!Sist-#na;>hS~I3LyyHb)Z4}pf&hDnaYZ2@L*f-oY^z8gGYAv
zW-2m~J$*3xM+9cQxap-UBO{00KdPwS6{59Mol3jq2aunDC}~4ffH-z5O7*osG8X`>hXQ>QxFDjr9ue9Byq~!r_&JlWeR=(f!i~cmC@kd9ldjA&CCuS
z*x8$@s*LQ(eLc2&|K*z#Shj8^a1>9~p%jP$FInP&_CN!Xu|TDwAIQZdSNL+3jWkaPxeS;!>>@j)T|5U^kj0>idr
z+w>9v>jxf)Q>s0;7muGPL?;C#eRf6^x?zj8OM->SQOKlH(NAx#`DR}0{)PRUSNGMv
z*Rg1HdrR-6;UANZZDj(F?&$pe!r-;)5^7JJdZiE%ms}@i6BGxt0>;P61foJ>$x%sZ
za`wwKZ1TIY4YY1KSt79Hk-6tDeBZygq}+3fMJ5}Ob2)+l2|~~T*3XyGieN=StWnn3
zi|d!4soVF(g8t^lzS`Dpmbuxh~(z>Ng;MdWK6?I)-?!7cLS_OhAg{U$Gq}UqY
z^*@ycKg4FoA)keuBVeOKqA)HhEe>6A7KYfmw)#&-WUcit{Wjdb{KR*S7q5*}NfEs$
zP&c%|50NkhL4eIpxIrxBlLDgT*u~tJNskwzSoN=2+E?5D`J&O+SM&m7BJ%uSI-#|8
zMlLq*esF%^$&Uvv*9?q|O&g8Th*0_{C5Gc#;6px(&F7%t)Y}_eFHjPh*mOVlK+U{Q
z>mPgYmLDT)tp_Xxo&sutO71Rlz>mOH5ox>2cTL4q{72&tR<;_ *oHJ)$&5J8qPe+xMh=B+I08(`|705lU{u2oQKCh+wNZbP#rl_q5
z0M+rtH`aLfH5jS}(FOp2P5?ll0O0Drh1dZAFF^poJ=ri_DKX%S$p2O0=p2Reo!fEdwyA$u
zrY!;VT&WXEdZ5RA%}lC6{=y>Ux1xO7;r_ZF4}+YrxKc)BZnt=X&(}(2t})V{q$Eld
zO35PH8eG)*W;Nxb`@O{{;p^`=%PyY?UPtxr-GYE9QdZA%>~eI@po+b#FX}G%j4=3i
zC&?3oW_&?_r9z3Dcp2*sew_SHkH;81OfgWDFZnl}{-$^S-NhQ=>F%XO5GyZm7_uu_
z(GHU|4+;X~O!WXg6iOo(g2LMnrD~Ad5JHb%(JTVuB$tByIXz_m=|9^cywQ&aY1DuJ
z0(q^Kxkq;=WCm>wlY-Bz+md{V71BjU$(1G;UOQ!2HUPhvaNyON}p1*B6y!w?;
z6al9{FhRh|Vol1s%I4@XbMuI52*Z}D-=OgpfnU_VMgkiywkQR!o&oy)>R3|Ha~NZ?
zWIUdlyUTdZsGfE^hHH#Hdz;6|zPclAhx
z&<7NnI5AI)2Q~o>TP{LJR#EbfW0*fafe|Kb*gQ?}0jvBWQqRQWAbEAXWF*;&G$U39;p7-M#~j_-&{snOHYLy1sYPEuJ=qLt
z&k;K?C4~mc9;F`?THu2@l!(|bbs`<%2BEXj-80pvsA0VZ4BIoz>bF0UQw+~322e^h
z?ZuIPxf$Kv1pDM53glFL?#PeG8Bg}#EBcBidxZ4Oxi^?Qqp4~fqA`}!b{Z5IN5zUh
zM{q@usC=xtxm5BY_zlP;y;50GQ!euT2nh*EjdMU_H8BScL|0e9QQU=t76l$_Qq{?Z
zI0AAD3T5r3Sp@&iU^#(MYQ{!*G0ZA~i{xT7|COpt;3
zzs~6@PALl{iK@to7;3W>NWQcvc}T~tK#Yq8YC5eU-eG21TeDp5O|>*n%NPjW=KGy`
zWYKS^o0WTJ?s-ryb2Y5tY;qMB#E3|QMCZyL3;M|Y6YZ<1dBCNhq0zNV-ca2Fl^!v;
zi9V$r-5&0g58DbPLpZYdW1A0}mXJeo)JnJG0ds=D;}<`6{Cb_(WC+hW6&f^31fZ8YD
z>?4GEPZk7nu}o?=-Us;Y70VCHXzq!3^w!hz+5H^~Njft>8Ii_nT3`HC=&^03?Q0+c
zv|L{qW3Hc+doFIILxcI#iU@lK1CP=_Kdr5MHtM#DCp>eRvN27rQ!i2Qt~j)e2x3%~
za+@tXo5>v3D;jRcUqHu$9ig$Q)mg(+W-waG4cwjfjGUznThuWW$p!Mmgj1}?7l6_n
zRK!nVOCvk{*(Ur+y6>?uf9c=QmkM^yq+TD24>Rw@8mlQ#A!~*MUt%FfE2OIM+ZtYr
z02U^VEAO816%>fV!Jo`(r0PHrD1#KUwu4@HvA?j^-!9jOg)(BX^-y)r1*quf>j@{c^BFeQ&wKH-I0gQN78x
z?uZJUq2m0C$2Q>$*?U)FYo{MySu;g%Ap=f=GUA!y#5TP6iEb`?%e1)vg;v(IJT>-6
zM#@LmmgMGnan85SAavQXt?5DR@%ZmV(dSMx@ll_CgeE7HxZ!znGN%ap*c>)sAmn5+
zx|>a06443mbV|MFI`SDM<=1X~F09rwy20u6=RaBhCGT|vlflNblFlstW<@G?d09#`
zHe!DQ|77o&!9{c@K6=SUuG|~_n>x`DoG9$2Om)Z*{a}H!{Y^?+-fU|}HD{bRPKFu>
zzlB~$b^lk^p|y3pzuB*JR};244a&^Pl_oPwzt9q}m4{=OHy0I_a_8c(3C}r{zg!C_+-6GJFXS`%bv<4)DkuZT4hL&QKX|n05?+{^F}aMN7o;kF=Q-fw`)8T-#Stx$
z!FN@PEjUxc2)?<}+Vj^1k_QB@f(>d-I=j@`>OLIFFqNyBX>W5Ix4Q1WoNBt;gNaC(
z*|-*p##9@RqYCf}GGTR*b*3+TXNvd&Pq!PhvO(5Xh3!L|KZ&E-yd=$Aer~zd*yDYjOPA4EkUe
z)Pt6V2Gc}OcX9svf}8*0iy<+g(y3vRN%L`2MoD8KHSez@tINI8rn70PslSL)7K`p}
z*qGI!^{Wrkd?B}Rh2s>z+Nss*4=AhtZUSvz3@Z@=E*)U(K2rIIncJ3_;}V(pkBh*%
zd*m9Fgz)%k^>FGIS}es=V$xZBN5%Q`8zXkq!L26)9*!!DFQBP=tcbSbm@
zQ}X1^Rpr$(tFh?z+wm-Ekw1=$EzA^8wY$FRY#NC`c;;w;axwWP4@vW|-V!&jN!Cp#
zdx11BY1>RK&B>nVJOhmoY9Ae-MI%J4SQ)p3PY(;Xy5x?C6cS34{Cc#@mHV_7;v`&<
zatl)AHpmEuW3hN0uuPyFd&oOy`)l{gy2rNj5*o#fP1DBcsoyMp=NcoFWCY)y3NFvp
zmU8y*_&~464pI~NSB~8^Fu}aDF29?yj%@rErRiV18cQSpO82{~NXphmGfNJ~$h2*jsycdhxxS6`PSF1OilJWR7R0R`&L>
zu=ZM86hRe1yBz0r-e$hXUk|5k2STxPT&djtNT!xAZ)nepc@7F
z+M!#^j&|#Lo)ij?J?)uj%2iC(D754+bMP$vgQ?zEs6UdCef;sdg!GtUB+%OZMp#fj
zZUoG6Ya-sri2Gw$1}){^ld0oc0^!=<&7XrANipB==KH?D6OGJ1oQ^
ztw1Jdafg*(7!q^Ngn-S$>-=R0AL^qlMSaFfSVPjVW5=xyk&RTJUb-nDQFuzoC~c{p
z&c=upn~aJMHXuS%2-n&@Cn1?6ZSB7=Z#P{ZpsoFmjq(J*Px
z_2o$9D?}A;%Byq2US&kMMoToNOauvc?=5%nqxT&E9rQOLQ24|GE|e~<3EHG)8aedG
zWo4dRQEjoIV!s`U7?Z`x_;>jGQP+;+0APPrXkc>sga1<6na8GY0d3}fq+CKogw1{%
ziblF$ih?wkM$GAdF+eyU%-HAFln{Q_T2OTKWXf7E#LpXh@suD3XM7#%ez@OxbntdN
zNPk~b<5TcZNOmhifT?;U&TPH-Q3Q08-n`hoirdbIzh*mQ*qBvp&M>dy?U5u6gBH(tLugyy^kr9z1Ji^+kU*voq!7!P_FgXl>wJ11IFN*tYD6nugg*C7t
zuS*DiW*?+DsPAw3ch%uMp$wnoa>6}@JyNkWVe-MMj&avpy~!$8;?@L;
zBBYBc_b*M2q0l#4zHV*L&x#-d$1r~B?QpW2cOy3S0{{MKxOD8~|ffhMlmUCI{e
z?x-FQe{y*^B~Zm%%Q#MFWBX{$=r1>SqREeW`N6tWq|j)?ms)$6d{WklD1mn$c!wud
z0Yt{L@oMYMSO0z*{*Q7gtar;yoC@oV*5$uPp|IndSu)2r?aX#54g;H8cq4}TO&Vb+LaZZ
zxnl!@m5m!xZ7NYKDt7W!)SZ;RCXaR*2Omh=LCMkN1dm2Lclbn%(fDe;13
zpt0OYXbV@8Ld^C!y?+FP+M$$kA(lJUu4EhmlI
zSqo0j^K3SwNP*f1-o^Z@lT=h}=GIZ!vKro6O>*qFoXR|YzEj?{udZgq>~YKETCY}H
zwClX~U6+c>YX{7(PaMdNaE5)hS)K=r$B0qkE7*_GPm!yabbx4uf^d!WIYS<7
zB<(`$zE?#9;MnRv|4eZ0LU+K5vT!3DE)8j1G+5?m72jA;27rinwiW-DWV~rcmO8zm
zkLde%jL&sIQE{e4DN3LWVfEo3ym=4@WX$(rX#Z~G2S!|ODn8!@_(TmiAzRr+l&9l*
zE$!1$lJ+~&bN3nhQ=@(Q-Dbz`A(@ggr(>6t(hSDd$iwFKuyxAF(&lRYBV
znrI%r=-Go}8hS(HCUgG=gFrN|V-5MA$-NA@EdNTj1I*hX<>xwb7YY`VA#9E1SHz7K
zX1W7vKZ5r@BcZRzJXexH!i-m>Gs3WqHGmt82^4Zc6Y`=>K-WGT*et=FNq9%~B72Bb
z)O;2}g#F@=v{B6$FB$OWqA(Y?02}VuWfT{YpUsFj%i_`yuer4GocL%Jjd%RpWDQ+9
zlh#%JsX&GP6aPSV^Lf}EzTPJXCsIUHtN~Au^a^44a^@GCYaK*@YL}m)vtjsk-Au>c
z_V*1_RqcG2w}l2bmCKfzlnVSrL#kLW=_oY1$01rFWn5B4&HAg%@v;k%`9iU|vn!?8
z)P0QE?auoph0SGcF@$TG|45@3+Vk>56CsjRNKLju;=#F=gcd_?Jk?9YY?R!jynfz|
zVFbhL6wJK5;i-B)KHydK|0vEOIn0r!ob@yVwrOZ|=k#=*3FPh$=e27gvCX>J=zrds
N`g0wX3MDJ#{{Yj@gK7W(
diff --git a/src/images/favicon-mask.svg b/src/images/favicon-mask.svg
deleted file mode 100644
index 821450a4..00000000
--- a/src/images/favicon-mask.svg
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
diff --git a/src/images/favicon.png b/src/images/favicon.png
new file mode 100644
index 0000000000000000000000000000000000000000..de87f852bb9b6d56e19ec80372fe736d79041ac4
GIT binary patch
literal 3706
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
+
+
+
+
diff --git a/src/images/kremalicious1024.png b/src/images/kremalicious1024.png
deleted file mode 100644
index 02e77b994193e62cc80b8fe45217a9d241c3d137..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 263997
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 ^T
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