Compare commits
220 Commits
4fcd74df8a
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 21c1d22859 | |||
| 68b328dabc | |||
| 817bd17959 | |||
| 77dd99ee2b | |||
| 857f33be54 | |||
| 320a98893a | |||
| e07fb3a72a | |||
| 1bb588d1d2 | |||
| aa1cb7411c | |||
| 5fe8c015f1 | |||
| 6f9538053c | |||
| 5e503cf3a5 | |||
| ce860cfef9 | |||
| 989755d33c | |||
| 9ddf52c635 | |||
| 9b62f79301 | |||
| 64aa9ec023 | |||
| 9bfd96af2c | |||
| 000af6934f | |||
| c94e36a861 | |||
| 2c831628b2 | |||
| 02fc666661 | |||
| c3cb224402 | |||
| 2b8ea1963b | |||
| f94880e758 | |||
| b6142cee44 | |||
| 53bfe5955d | |||
| 9f503c011d | |||
| 415d95ed47 | |||
| e787544a2b | |||
| 9f94164c61 | |||
| 608ccfdecd | |||
| 933652fd57 | |||
| fdaed1033a | |||
| bc77baca2e | |||
| 7c9a55224a | |||
| 85ba3747b1 | |||
| 71f8629d18 | |||
| b1492918d5 | |||
| 95ea38d250 | |||
| 81be69f08d | |||
| 71eaa3e788 | |||
| 5c75f1416f | |||
| 3379e50503 | |||
| 30c8937566 | |||
| 770a96b099 | |||
| 604ba00c70 | |||
| b6c9d0b2ac | |||
| 51ab99b839 | |||
| 2f733e5ffc | |||
| d7e3162f55 | |||
| 8e127e7e3a | |||
| e853cb543a | |||
| e29953ab90 | |||
| 366974475f | |||
| fa29888578 | |||
| 31f3c6fb2f | |||
| 936662e35c | |||
| a440583691 | |||
| 6486d377b2 | |||
| 6c842ebe7f | |||
| 89c9771a84 | |||
| 89ae41d9ec | |||
| 3983615c99 | |||
| 8412d1540c | |||
| 3f53bf5b85 | |||
| 3c77d6cdad | |||
| e44105b330 | |||
| f6a8657de2 | |||
| 89e2708b1e | |||
| f78ab147af | |||
| 886ed21e5d | |||
| eafc431e0e | |||
| 9809950347 | |||
| 21b572677e | |||
| 7c2303c4e8 | |||
| a4b3e526fb | |||
| 3018ae16ff | |||
| ff9ea3a0a7 | |||
| 913e4bf19a | |||
| 6eaa00d612 | |||
| 04e4fa3dcd | |||
| 8edbfd2dd3 | |||
| b1cffca953 | |||
| cf364bc298 | |||
| 5a6c00eaa4 | |||
| 512f1ce9b2 | |||
| ee107eebdf | |||
| 63a826fc8e | |||
| a9ce9a257c | |||
| 5ee0b8510f | |||
| 19d34622ca | |||
| dd764c8726 | |||
| dacda6fca0 | |||
| 8f87155c1d | |||
| 42ed59a6b3 | |||
| c403ea9593 | |||
| a2cdbd7506 | |||
| f463eadbef | |||
| ce5d520817 | |||
| b1e1a5cb9a | |||
| a7786f263f | |||
| ffcf156289 | |||
| d923f3eb46 | |||
| 075a8fa9d4 | |||
| 20212fee25 | |||
| 229532ab8b | |||
| 138649c8e5 | |||
| 728a43c4c3 | |||
| 850d2f5c50 | |||
| 6283c840ff | |||
| 7a9cd0f269 | |||
| cf5e1ecb2d | |||
| da7fbaf5b1 | |||
| e7482e5bdd | |||
| f829da10ec | |||
| fb5ae6732c | |||
| a398bcb737 | |||
| 9365f46440 | |||
| 246fbfde76 | |||
| 2a151b710c | |||
| ca283d621a | |||
| ca920a9fe8 | |||
| 26182ec363 | |||
| d0c821588e | |||
| 3edc18fe28 | |||
| 5bc8d008df | |||
| 5eca310bd8 | |||
| 13d6576a2c | |||
| bc67a0ee88 | |||
| 46c8a76633 | |||
| cc341cc944 | |||
| f4ee63282b | |||
| 326f28e4ac | |||
| 6e5caf33ad | |||
| 49c4ab0341 | |||
| 7dcaa703e0 | |||
| a3565677a5 | |||
| 37c38e925a | |||
| 3301f049cc | |||
| b1665dad80 | |||
| d9fd5eb74c | |||
| dfca8ef6e2 | |||
| 6ce77d7be7 | |||
| 2adf06831c | |||
| 3772a64a0e | |||
| 3bd1e61817 | |||
| 14e386a122 | |||
| 8152fe79b6 | |||
| 1a247e1889 | |||
| 103ceb62b9 | |||
| 3845d1b5e4 | |||
| c123a035ce | |||
| dfd1c38396 | |||
| 48b877c439 | |||
| 0dc9095b4b | |||
| fcdb3de387 | |||
| 3b5dc18ec6 | |||
| a06f744ec1 | |||
| c514bfd4a9 | |||
| 916969c96f | |||
| 3ef8d48ee2 | |||
| 997baf4cc3 | |||
| 456fc94c8e | |||
| 044e74f5d3 | |||
| f7df6ef37e | |||
| a363052f5f | |||
| b431cfc0ac | |||
| 87a782ae12 | |||
| 12c5b2c4a1 | |||
| 0d1688c6c4 | |||
| a9043f711e | |||
| 93005bd7cd | |||
| fe0aa669bc | |||
| 897da36a21 | |||
| eb739d80ab | |||
| 0478a18fa8 | |||
| 2508936928 | |||
| 650e97883b | |||
| 2eef8fbf9a | |||
| 11224289de | |||
| 69c9f4f939 | |||
| 010478b3fa | |||
| 49d10f4816 | |||
| a9eda558c0 | |||
| 16b44513f2 | |||
| ab8a5138dd | |||
| b66f1cdb2d | |||
| a78236bf3b | |||
| a9843a0a2d | |||
| 5c98bf239a | |||
| 86b2778a47 | |||
| 035c92f293 | |||
| fbc4fc195b | |||
| 597add6c1d | |||
| 1c9a6711b3 | |||
| 537f443cf1 | |||
| e4451857c2 | |||
| feeef865aa | |||
| 5c02432ce0 | |||
| d3ef42f04f | |||
| bae9d68943 | |||
| dc162ff58c | |||
| 3d5e29e26c | |||
| ba3a2ea9e7 | |||
| 64b7fcc166 | |||
| 0bb3b3bcce | |||
| a1acabbf17 | |||
| 70b4e1ca7a | |||
| 24f3c14d77 | |||
| d1066d7eb3 | |||
| ffda4568ab | |||
| 86997cb878 | |||
| 50a5f2d178 | |||
| 2a32917568 | |||
| 24acae2a85 | |||
| 534b9a96f1 | |||
| 3a79fe2cc7 | |||
| 9a9220e066 | |||
| c05b9e3400 |
@@ -1,3 +1,9 @@
|
|||||||
/plugins/
|
/plugins/*
|
||||||
|
!/plugins/.gitkeep
|
||||||
!/plugins/cache-on-save/
|
!/plugins/cache-on-save/
|
||||||
|
!/plugins/story-blocks/
|
||||||
/data/
|
/data/
|
||||||
|
/pages/01.trips/italy-2026-demo/
|
||||||
|
/pages/02.post/*ui-test*/
|
||||||
|
/config/plugins/git-sync.yaml
|
||||||
|
/config/security.yaml
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
/* @copyright Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved. */
|
||||||
@@ -1,13 +1,27 @@
|
|||||||
login: mischa
|
login: mischa
|
||||||
|
state: enabled
|
||||||
|
title: ''
|
||||||
|
email: mischa@gorinskat.nl
|
||||||
|
fullname: Mischa
|
||||||
|
hashed_password: $2y$10$xyV7bAUWEo75K6LbatUuYe/6x2Tj9nT6YnIjaDvESAhU2hJ7tjG2.
|
||||||
|
language: en
|
||||||
|
modified: 1782051328
|
||||||
|
admin_next:
|
||||||
|
preferences:
|
||||||
|
pluginsViewMode: cards
|
||||||
|
colorMode: dark
|
||||||
|
accentHue: 271
|
||||||
|
accentSaturation: 91
|
||||||
|
fontFamily: inter
|
||||||
|
pagesViewMode: tree
|
||||||
|
content_editor: ''
|
||||||
|
groups: { }
|
||||||
access:
|
access:
|
||||||
admin:
|
admin:
|
||||||
login: true
|
login: true
|
||||||
super: true
|
super: true
|
||||||
site:
|
site:
|
||||||
login: true
|
login: true
|
||||||
state: enabled
|
api:
|
||||||
title: Mischa
|
super: true
|
||||||
email: mischa@gorinskat.nl
|
access: true
|
||||||
fullname: Mischa
|
|
||||||
hashed_password: '$2y$10$dUEYTopGEDouFoAa/Wxw6.vsOA71yr3gSStfDvr10aKm4ih9ObQ7m'
|
|
||||||
language: en
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
form:
|
||||||
|
validation: loose
|
||||||
|
fields:
|
||||||
|
active_trip:
|
||||||
|
type: pages
|
||||||
|
label: Active Trip
|
||||||
|
start_route: '/trips'
|
||||||
|
show_root: false
|
||||||
|
show_slug: true
|
||||||
|
|
||||||
|
travelling:
|
||||||
|
type: toggle
|
||||||
|
label: Currently Travelling
|
||||||
|
highlight: 1
|
||||||
|
default: false
|
||||||
|
options:
|
||||||
|
1: 'Yes'
|
||||||
|
0: 'No'
|
||||||
|
validate:
|
||||||
|
type: bool
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
name: Daily Entry
|
|
||||||
extends@: default
|
|
||||||
|
|
||||||
form:
|
|
||||||
fields:
|
|
||||||
tabs:
|
|
||||||
type: tabs
|
|
||||||
active: 1
|
|
||||||
fields:
|
|
||||||
content:
|
|
||||||
type: tab
|
|
||||||
title: Entry
|
|
||||||
|
|
||||||
fields:
|
|
||||||
header.title:
|
|
||||||
type: text
|
|
||||||
label: Title
|
|
||||||
validate:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
header.date:
|
|
||||||
type: datetime
|
|
||||||
label: Date
|
|
||||||
format: 'Y-m-d H:i'
|
|
||||||
validate:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
content:
|
|
||||||
type: markdown
|
|
||||||
label: Content
|
|
||||||
validate:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
header.hero_image:
|
|
||||||
type: text
|
|
||||||
label: 'Hero Image Filename'
|
|
||||||
help: 'Filename of the main photo for this entry (e.g. photo.jpg). Upload photos via the Media tab.'
|
|
||||||
|
|
||||||
location:
|
|
||||||
type: tab
|
|
||||||
title: Location
|
|
||||||
|
|
||||||
fields:
|
|
||||||
header.location_city:
|
|
||||||
type: text
|
|
||||||
label: City
|
|
||||||
placeholder: 'e.g. Kyoto'
|
|
||||||
|
|
||||||
header.location_country:
|
|
||||||
type: text
|
|
||||||
label: Country
|
|
||||||
placeholder: 'e.g. Japan'
|
|
||||||
|
|
||||||
header.lat:
|
|
||||||
type: number
|
|
||||||
label: Latitude
|
|
||||||
help: 'GPS latitude (for map)'
|
|
||||||
placeholder: '48.8566'
|
|
||||||
step: any
|
|
||||||
|
|
||||||
header.lng:
|
|
||||||
type: number
|
|
||||||
label: Longitude
|
|
||||||
help: 'GPS longitude (for map)'
|
|
||||||
placeholder: '2.3522'
|
|
||||||
step: any
|
|
||||||
|
|
||||||
weather:
|
|
||||||
type: tab
|
|
||||||
title: Weather
|
|
||||||
|
|
||||||
fields:
|
|
||||||
header.weather_temp_c:
|
|
||||||
type: number
|
|
||||||
label: 'Temperature (°C)'
|
|
||||||
help: 'Auto-filled from post form. Edit if needed.'
|
|
||||||
step: 1
|
|
||||||
|
|
||||||
header.weather_desc:
|
|
||||||
type: select
|
|
||||||
label: 'Weather Condition'
|
|
||||||
options:
|
|
||||||
Sunny: '☀️ Sunny'
|
|
||||||
'Partly cloudy': '⛅ Partly cloudy'
|
|
||||||
Cloudy: '☁️ Cloudy'
|
|
||||||
Foggy: '🌫️ Foggy'
|
|
||||||
Drizzle: '🌦️ Drizzle'
|
|
||||||
Rain: '🌧️ Rain'
|
|
||||||
Snow: '❄️ Snow'
|
|
||||||
Thunderstorm: '⛈️ Thunderstorm'
|
|
||||||
|
|
||||||
publishing:
|
|
||||||
type: tab
|
|
||||||
title: Publishing
|
|
||||||
|
|
||||||
fields:
|
|
||||||
header.published:
|
|
||||||
type: toggle
|
|
||||||
label: Published
|
|
||||||
highlight: 1
|
|
||||||
default: 1
|
|
||||||
options:
|
|
||||||
1: 'Yes'
|
|
||||||
0: 'No'
|
|
||||||
validate:
|
|
||||||
type: bool
|
|
||||||
|
|
||||||
header.publish_date:
|
|
||||||
type: datetime
|
|
||||||
label: 'Publish Date'
|
|
||||||
help: 'Schedule future publication (leave blank to publish immediately)'
|
|
||||||
format: 'Y-m-d H:i'
|
|
||||||
|
|
||||||
header.unpublish_date:
|
|
||||||
type: datetime
|
|
||||||
label: 'Unpublish Date'
|
|
||||||
help: 'Automatically unpublish at this date/time'
|
|
||||||
format: 'Y-m-d H:i'
|
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
ui:
|
||||||
|
defaults:
|
||||||
|
colorMode: ''
|
||||||
|
accentHue: 271
|
||||||
|
accentSaturation: 91
|
||||||
|
fontFamily: inter
|
||||||
|
fontSize: normal
|
||||||
|
editorMode: normal
|
||||||
|
editorStickyToolbar: true
|
||||||
|
editorFixedHeight: 0
|
||||||
|
adminLanguage: en-US
|
||||||
|
pagesPerPage: 20
|
||||||
|
pagesViewMode: tree
|
||||||
|
usersViewMode: cards
|
||||||
|
groupsViewMode: cards
|
||||||
|
pluginsViewMode: cards
|
||||||
|
themesViewMode: cards
|
||||||
|
settings:
|
||||||
|
autoSaveEnabled: false
|
||||||
|
autoSaveToolbarUndo: true
|
||||||
|
autoSaveBatchWindowMs: 0
|
||||||
|
collabEnabled: false
|
||||||
|
menubarLinks: { }
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
types:
|
||||||
|
defaults:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb.png
|
||||||
|
mime: application/octet-stream
|
||||||
|
image:
|
||||||
|
filters:
|
||||||
|
default:
|
||||||
|
- enableProgressive
|
||||||
|
|
||||||
|
jpg:
|
||||||
|
type: image
|
||||||
|
thumb: media/thumb-jpg.png
|
||||||
|
mime: image/jpeg
|
||||||
|
jpe:
|
||||||
|
type: image
|
||||||
|
thumb: media/thumb-jpg.png
|
||||||
|
mime: image/jpeg
|
||||||
|
jpeg:
|
||||||
|
type: image
|
||||||
|
thumb: media/thumb-jpg.png
|
||||||
|
mime: image/jpeg
|
||||||
|
png:
|
||||||
|
type: image
|
||||||
|
thumb: media/thumb-png.png
|
||||||
|
mime: image/png
|
||||||
|
webp:
|
||||||
|
type: image
|
||||||
|
thumb: media/thumb-webp.png
|
||||||
|
mime: image/webp
|
||||||
|
avif:
|
||||||
|
type: image
|
||||||
|
thumb: media/thumb.png
|
||||||
|
mime: image/avif
|
||||||
|
gif:
|
||||||
|
type: animated
|
||||||
|
thumb: media/thumb-gif.png
|
||||||
|
mime: image/gif
|
||||||
|
svg:
|
||||||
|
type: vector
|
||||||
|
thumb: media/thumb-svg.png
|
||||||
|
mime: image/svg+xml
|
||||||
|
mp4:
|
||||||
|
type: video
|
||||||
|
thumb: media/thumb-mp4.png
|
||||||
|
mime: video/mp4
|
||||||
|
mov:
|
||||||
|
type: video
|
||||||
|
thumb: media/thumb-mov.png
|
||||||
|
mime: video/quicktime
|
||||||
|
m4v:
|
||||||
|
type: video
|
||||||
|
thumb: media/thumb-m4v.png
|
||||||
|
mime: video/x-m4v
|
||||||
|
swf:
|
||||||
|
type: video
|
||||||
|
thumb: media/thumb-swf.png
|
||||||
|
mime: video/x-flv
|
||||||
|
flv:
|
||||||
|
type: video
|
||||||
|
thumb: media/thumb-flv.png
|
||||||
|
mime: video/x-flv
|
||||||
|
webm:
|
||||||
|
type: video
|
||||||
|
thumb: media/thumb-webm.png
|
||||||
|
mime: video/webm
|
||||||
|
ogv:
|
||||||
|
type: video
|
||||||
|
thumb: media/thumb-ogg.png
|
||||||
|
mime: video/ogg
|
||||||
|
mp3:
|
||||||
|
type: audio
|
||||||
|
thumb: media/thumb-mp3.png
|
||||||
|
mime: audio/mp3
|
||||||
|
ogg:
|
||||||
|
type: audio
|
||||||
|
thumb: media/thumb-ogg.png
|
||||||
|
mime: audio/ogg
|
||||||
|
wma:
|
||||||
|
type: audio
|
||||||
|
thumb: media/thumb-wma.png
|
||||||
|
mime: audio/wma
|
||||||
|
m4a:
|
||||||
|
type: audio
|
||||||
|
thumb: media/thumb-m4a.png
|
||||||
|
mime: audio/m4a
|
||||||
|
wav:
|
||||||
|
type: audio
|
||||||
|
thumb: media/thumb-wav.png
|
||||||
|
mime: audio/wav
|
||||||
|
aiff:
|
||||||
|
type: audio
|
||||||
|
thumb: media/thumb-aif.png
|
||||||
|
mime: audio/aiff
|
||||||
|
aif:
|
||||||
|
type: audio
|
||||||
|
thumb: media/thumb-aif.png
|
||||||
|
mime: audio/aiff
|
||||||
|
txt:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb-txt.png
|
||||||
|
mime: text/plain
|
||||||
|
xml:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb-xml.png
|
||||||
|
mime: application/xml
|
||||||
|
doc:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb-doc.png
|
||||||
|
mime: application/msword
|
||||||
|
docx:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb-docx.png
|
||||||
|
mime: application/vnd.openxmlformats-officedocument.wordprocessingml.document
|
||||||
|
xls:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb-xls.png
|
||||||
|
mime: application/vnd.ms-excel
|
||||||
|
xlsx:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb-xlsx.png
|
||||||
|
mime: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
|
||||||
|
ppt:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb-ppt.png
|
||||||
|
mime: application/vnd.ms-powerpoint
|
||||||
|
pptx:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb-pptx.png
|
||||||
|
mime: application/vnd.openxmlformats-officedocument.presentationml.presentation
|
||||||
|
pps:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb-pps.png
|
||||||
|
mime: application/vnd.ms-powerpoint
|
||||||
|
rtf:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb-rtf.png
|
||||||
|
mime: application/rtf
|
||||||
|
bmp:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb-bmp.png
|
||||||
|
mime: image/bmp
|
||||||
|
tiff:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb-tiff.png
|
||||||
|
mime: image/tiff
|
||||||
|
mpeg:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb-mpg.png
|
||||||
|
mime: video/mpeg
|
||||||
|
mpg:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb-mpg.png
|
||||||
|
mime: video/mpeg
|
||||||
|
mpe:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb-mpe.png
|
||||||
|
mime: video/mpeg
|
||||||
|
avi:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb-avi.png
|
||||||
|
mime: video/msvideo
|
||||||
|
wmv:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb-wmv.png
|
||||||
|
mime: video/x-ms-wmv
|
||||||
|
html:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb-html.png
|
||||||
|
mime: text/html
|
||||||
|
htm:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb-html.png
|
||||||
|
mime: text/html
|
||||||
|
ics:
|
||||||
|
type: iCal
|
||||||
|
thumb: media/thumb-ics.png
|
||||||
|
mime: text/calendar
|
||||||
|
pdf:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb-pdf.png
|
||||||
|
mime: application/pdf
|
||||||
|
ai:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb-ai.png
|
||||||
|
mime: image/ai
|
||||||
|
psd:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb-psd.png
|
||||||
|
mime: image/psd
|
||||||
|
zip:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb-zip.png
|
||||||
|
mime: application/zip
|
||||||
|
7z:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb-7z.png
|
||||||
|
mime: application/x-7z-compressed
|
||||||
|
gz:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb-gz.png
|
||||||
|
mime: application/x-gzip
|
||||||
|
tar:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb-tar.png
|
||||||
|
mime: application/x-tar
|
||||||
|
css:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb-css.png
|
||||||
|
mime: text/css
|
||||||
|
js:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb-js.png
|
||||||
|
mime: text/javascript
|
||||||
|
json:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb-json.png
|
||||||
|
mime: application/json
|
||||||
|
vcf:
|
||||||
|
type: file
|
||||||
|
thumb: media/thumb-vcf.png
|
||||||
|
mime: text/x-vcard
|
||||||
|
gpx:
|
||||||
|
type: file
|
||||||
|
mime: application/gpx+xml
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
popularity:
|
||||||
|
salt: 671ae9ab4f792c7dc860dbe8be288f2fdebdb3b4615f3c0f43211ecb95aaeeb3
|
||||||
|
auth:
|
||||||
|
jwt_secret: 61a84160bdd430768c82c4fe153e151a7a6f68f993c3779a5f36d32ee9293653
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// Auto-generated private secret. Do NOT commit to version control.
|
||||||
|
// Used for CSRF nonce signing and admin rate-limit hashing. Regenerate by
|
||||||
|
// deleting this file; the next request will write a new value.
|
||||||
|
|
||||||
|
return 'lsUHWFkCwvGZrL';
|
||||||
@@ -1 +0,0 @@
|
|||||||
salt: lsUHWFkCwvGZrL
|
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
title: 'Into the East'
|
title: 'Into the East'
|
||||||
description: 'A travel blog by Mischa'
|
|
||||||
author:
|
author:
|
||||||
name: Mischa
|
name: Mischa
|
||||||
email: mischa@gorinskat.nl
|
email: mischa@gorinskat.nl
|
||||||
taxonomies: [category, tag]
|
|
||||||
metadata:
|
metadata:
|
||||||
description: 'Into the East — travel journal'
|
description: 'Into the East — travel journal'
|
||||||
|
description: 'A travel blog by Mischa'
|
||||||
|
active_trip: /trips/us-canada-mex-2024
|
||||||
|
travelling: false
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ wrapped_site: false
|
|||||||
reverse_proxy_setup: false
|
reverse_proxy_setup: false
|
||||||
force_ssl: false
|
force_ssl: false
|
||||||
force_lowercase_urls: true
|
force_lowercase_urls: true
|
||||||
custom_base_url: 'http://100.96.115.96:8081'
|
custom_base_url: ''
|
||||||
username_regex: '^[a-z0-9_-]{3,16}$'
|
username_regex: '^[a-z0-9_-]{3,16}$'
|
||||||
pwd_regex: '(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}'
|
pwd_regex: '(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}'
|
||||||
intl_enabled: true
|
intl_enabled: true
|
||||||
@@ -28,10 +28,10 @@ languages:
|
|||||||
pages_fallback_only: false
|
pages_fallback_only: false
|
||||||
debug: false
|
debug: false
|
||||||
home:
|
home:
|
||||||
alias: /tracker
|
alias: /home
|
||||||
hide_in_urls: false
|
hide_in_urls: false
|
||||||
pages:
|
pages:
|
||||||
type: regular
|
type: flex
|
||||||
dirs:
|
dirs:
|
||||||
- 'page://'
|
- 'page://'
|
||||||
theme: intotheeast
|
theme: intotheeast
|
||||||
@@ -41,7 +41,7 @@ pages:
|
|||||||
list:
|
list:
|
||||||
count: 20
|
count: 20
|
||||||
dateformat:
|
dateformat:
|
||||||
default: 'd M Y'
|
default: 'Y-m-d H:i'
|
||||||
short: 'D, d M Y G:i:s'
|
short: 'D, d M Y G:i:s'
|
||||||
long: 'D, d M Y G:i:s'
|
long: 'D, d M Y G:i:s'
|
||||||
publish_dates: true
|
publish_dates: true
|
||||||
@@ -210,7 +210,7 @@ session:
|
|||||||
domain: null
|
domain: null
|
||||||
path: null
|
path: null
|
||||||
gpm:
|
gpm:
|
||||||
releases: stable
|
releases: testing
|
||||||
official_gpm_only: true
|
official_gpm_only: true
|
||||||
http:
|
http:
|
||||||
method: curl
|
method: curl
|
||||||
@@ -221,7 +221,7 @@ http:
|
|||||||
verify_peer: true
|
verify_peer: true
|
||||||
verify_host: true
|
verify_host: true
|
||||||
accounts:
|
accounts:
|
||||||
type: regular
|
type: flex
|
||||||
storage: file
|
storage: file
|
||||||
avatar: gravatar
|
avatar: gravatar
|
||||||
flex:
|
flex:
|
||||||
|
|||||||
@@ -1,136 +0,0 @@
|
|||||||
# Bugs & Fixes
|
|
||||||
|
|
||||||
Backlog of confirmed bugs with root cause analysis and implementation spec for the fix.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## BUG-001 — New entry not visible after form submission
|
|
||||||
|
|
||||||
**Status:** fixed 2026-06-18
|
|
||||||
**Reported:** 2026-06-18
|
|
||||||
|
|
||||||
### Symptom
|
|
||||||
|
|
||||||
After submitting a new post via `/post`, the entry page file is created correctly on disk but does not appear in the `/tracker` feed or in the Grav Admin panel until the cache is manually flushed.
|
|
||||||
|
|
||||||
### Root cause
|
|
||||||
|
|
||||||
Grav's page-tree cache (`cache/doctrine/`) is not invalidated when `add-page-by-form` writes a new page to disk. The tracker template uses `page.children`, which Grav serves from cache — so the new child page is invisible until the cache is cleared.
|
|
||||||
|
|
||||||
### Workaround (manual)
|
|
||||||
|
|
||||||
Run in terminal after each submission:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec intotheeast_grav bash -c "cd /app/www/public && php bin/grav clearcache"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Fix spec
|
|
||||||
|
|
||||||
Wire cache-clear into the form process so it happens automatically on every successful submission.
|
|
||||||
|
|
||||||
**Approach — custom Grav plugin event hook:**
|
|
||||||
|
|
||||||
1. Create a small plugin `user/plugins/cache-on-save/` with one event listener:
|
|
||||||
- Listen on `onFormProcessed`
|
|
||||||
- When the form name is `new-entry`, call `$this->grav['cache']->deleteAll()` (note: `clear()` does not exist on `Grav\Common\Cache` in Grav 1.7)
|
|
||||||
2. Enable the plugin in `user/config/plugins/cache-on-save.yaml`
|
|
||||||
|
|
||||||
This is the cleanest approach: it fires exactly once per successful submission, requires no changes to `post-form.md`, and works for any future forms too.
|
|
||||||
|
|
||||||
**Alternative — disable page cache entirely:**
|
|
||||||
|
|
||||||
Set `cache: { enabled: false }` in `system.yaml`. Simpler but degrades frontend performance; not recommended for production.
|
|
||||||
|
|
||||||
### Files to create/modify
|
|
||||||
|
|
||||||
| File | Change |
|
|
||||||
|------|--------|
|
|
||||||
| `user/plugins/cache-on-save/cache-on-save.php` | New plugin, ~30 lines |
|
|
||||||
| `user/plugins/cache-on-save/cache-on-save.yaml` | Plugin manifest, enabled: true |
|
|
||||||
| `user/config/plugins/cache-on-save.yaml` | Runtime config, enabled: true |
|
|
||||||
|
|
||||||
### Acceptance criteria
|
|
||||||
|
|
||||||
1. Submit a new post via `/post`
|
|
||||||
2. Navigate to `/tracker` — the new entry is visible immediately, no manual cache flush needed
|
|
||||||
3. Grav Admin also shows the new page immediately
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## BUG-002 — Stale Twig cache after theme file changes
|
|
||||||
|
|
||||||
**Status:** fixed 2026-06-18
|
|
||||||
**Reported:** 2026-06-18
|
|
||||||
|
|
||||||
### Symptom
|
|
||||||
|
|
||||||
After theme template files are added or modified (e.g., creating `partials/base.html.twig`), Grav's Twig compiled-template cache still holds the old compiled version. Pages that extend the changed file throw 500 errors like "Template partials/base.html.twig is not defined" even though the file exists on disk.
|
|
||||||
|
|
||||||
### Root cause
|
|
||||||
|
|
||||||
Grav caches compiled Twig templates in `cache/twig/`. When a new file is added, existing templates that reference it don't know to recompile — their cache entries are still valid from their own mtime perspective.
|
|
||||||
|
|
||||||
### Workaround (manual)
|
|
||||||
|
|
||||||
Run after any theme file is added or changed:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker exec intotheeast_grav bash -c "cd /app/www/public && php bin/grav clearcache"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Fix spec
|
|
||||||
|
|
||||||
Disable Twig template caching in development via `user/config/system.yaml`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
twig:
|
|
||||||
cache: false
|
|
||||||
```
|
|
||||||
|
|
||||||
Acceptable for a single-user dev setup — eliminates both BUG-001's side-effect and this bug entirely. Performance cost is negligible at one-user scale. On production, leave Twig cache enabled (it's fine there because template files don't change at runtime).
|
|
||||||
|
|
||||||
**Files to change:**
|
|
||||||
|
|
||||||
| File | Change |
|
|
||||||
|------|--------|
|
|
||||||
| `user/config/system.yaml` | Add `twig: { cache: false }` under development section |
|
|
||||||
|
|
||||||
### Acceptance criteria
|
|
||||||
|
|
||||||
1. Add a new theme template file
|
|
||||||
2. Reload any page — no 500 error, template works immediately without manual cache flush
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## BUG-003 — One post per day limit; silent failure on duplicate date
|
|
||||||
|
|
||||||
**Status:** fixed 2026-06-18
|
|
||||||
**Reported:** 2026-06-18
|
|
||||||
|
|
||||||
### Symptom
|
|
||||||
|
|
||||||
Submitting a second post with the same date as an existing entry shows "Entry posted successfully!" but creates no file. The user's post is silently discarded.
|
|
||||||
|
|
||||||
### Root cause
|
|
||||||
|
|
||||||
The `add-page-by-form` plugin built the page slug from date only (`Y-m-d`), producing folder names like `2026-06-18.entry`. With `overwrite_mode: false`, if that folder already exists the plugin skips page creation but does not abort — the `message` process step runs regardless, showing a false success.
|
|
||||||
|
|
||||||
### Fix
|
|
||||||
|
|
||||||
Change the slug template in `user/pages/02.post/post-form.md` to include time and title:
|
|
||||||
|
|
||||||
```twig
|
|
||||||
{{ form.value.date|date('Y-m-d-Hi') }}-{{ form.value.title|lower|regex_replace('/[^a-z0-9]+/', '-')|trim('-') }}
|
|
||||||
```
|
|
||||||
|
|
||||||
Example: title "Arrived in Tokyo" at 14:30 on 2026-06-18 → `2026-06-18-1430-arrived-in-tokyo`
|
|
||||||
|
|
||||||
The slug is locked at creation time. Renaming the title afterwards does not change the URL.
|
|
||||||
|
|
||||||
### Acceptance criteria
|
|
||||||
|
|
||||||
1. Submit two posts on the same day with different times or titles — both appear in `/tracker` as separate entries
|
|
||||||
2. Renaming a post's title in the frontmatter does not break its URL
|
|
||||||
|
|
||||||
---
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
title: 'Wheels Down at Narita'
|
|
||||||
date: '2026-03-25 15:40'
|
|
||||||
template: entry
|
|
||||||
published: true
|
|
||||||
hero_image: ''
|
|
||||||
lat: '35.7720'
|
|
||||||
lng: '140.3929'
|
|
||||||
location_city: 'Tokyo'
|
|
||||||
location_country: 'Japan'
|
|
||||||
weather_temp_c: 16
|
|
||||||
weather_desc: 'Sunny'
|
|
||||||
---
|
|
||||||
|
|
||||||
Eleven hours of flight time, two mediocre films, and one surprisingly good noodle dish from the trolley. Then the descent through scattered cloud, the first glimpse of grey-green patchwork below, and that particular feeling when the wheels finally touch down on a continent you have never stood on before.
|
|
||||||
|
|
||||||
Narita is large and orderly and very, very calm. Immigration moved faster than any airport I have ever been through. The officer looked at my passport, looked at me, stamped it once, and handed it back without a word. That was it. Entry to Japan.
|
|
||||||
|
|
||||||
The Narita Express runs direct to Shinjuku. I found a window seat and spent 90 minutes watching the city materialise from the outside in — rice fields giving way to low housing, then arterial roads, then the sudden verticality of central Tokyo rising up all at once as if someone just switched a setting.
|
|
||||||
|
|
||||||
The hotel is small but perfect. A room roughly the width of my arms outstretched, a window looking onto a grey concrete wall, and a bed that feels like sleeping on a cloud. I went out for ramen at a place around the corner where you order from a vending machine and sit at a counter alone with a small wooden partition between you and the next person. Nobody spoke. It was the best meal I have had in months.
|
|
||||||
|
|
||||||
Tomorrow: Ueno. The forecast says the cherry blossoms may finally be open.
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
title: 'Sakura in Ueno Park'
|
|
||||||
date: '2026-03-26 10:00'
|
|
||||||
template: entry
|
|
||||||
published: true
|
|
||||||
hero_image: ''
|
|
||||||
lat: '35.7155'
|
|
||||||
lng: '139.7753'
|
|
||||||
location_city: 'Tokyo'
|
|
||||||
location_country: 'Japan'
|
|
||||||
weather_temp_c: 14
|
|
||||||
weather_desc: 'Partly cloudy'
|
|
||||||
---
|
|
||||||
|
|
||||||
I arrived at Ueno Park at ten in the morning thinking I would beat the crowds. I was wrong. Several thousand people had the same idea, and the same Instagram instinct. But here is the thing about cherry blossom season in Japan — the crowds are almost part of it. Families with picnic sheets. Couples with matching outfits. Office workers in suits sitting on blue tarps eating convenience-store onigiri. Everyone doing the same thing: looking up at the same trees.
|
|
||||||
|
|
||||||
The blossoms were at maybe seventy percent. Enough to understand what the fuss is about.
|
|
||||||
|
|
||||||
I walked the park from one end to the other and then sat under a particularly generous tree for about an hour just watching people react to something beautiful. There is a Japanese word for it — *hanami* — which translates roughly as "flower viewing" and is more or less an entire cultural practice. You do not rush past the blossoms. You sit with them.
|
|
||||||
|
|
||||||
Later I found the Tokyo National Museum at the top of the park. Three floors of Japanese history, almost entirely in Japanese, which I cannot read, but context is its own language. A display case of Edo-period swords. Painted screens showing mountains I now recognise. A reconstructed tea house in the garden, closed for the season but visible through the glass.
|
|
||||||
|
|
||||||
Dinner: tonkatsu on a side street off Ueno-Okachimachi station. The woman who runs the counter has been there for at least thirty years by the look of it. She refilled my miso soup without being asked, twice.
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
---
|
|
||||||
title: 'Summit Clouds and Snow'
|
|
||||||
date: '2026-03-27 07:15'
|
|
||||||
template: entry
|
|
||||||
published: true
|
|
||||||
hero_image: ''
|
|
||||||
lat: '35.5095'
|
|
||||||
lng: '138.7646'
|
|
||||||
location_city: 'Kawaguchiko'
|
|
||||||
location_country: 'Japan'
|
|
||||||
weather_temp_c: 1
|
|
||||||
weather_desc: 'Snow'
|
|
||||||
---
|
|
||||||
|
|
||||||
Nobody told me it would snow.
|
|
||||||
|
|
||||||
I took the early bus from Shinjuku at 6:45am because the forecast for the Fuji Five Lakes region said "clear morning, clouds by noon." That is the window you want — Fuji is notorious for hiding inside its own weather system, and most visitors spend an entire day staring at a blank white sky where a mountain ought to be.
|
|
||||||
|
|
||||||
I got the mountain. For about forty minutes.
|
|
||||||
|
|
||||||
By the time the bus pulled into Kawaguchiko, the first flakes were already coming down. Light at first — the decorative kind that you hold your hand out for. Then, steadily, not decorative at all. I walked down to the lake with my bag under my jacket and stood at the water's edge while the snow thickened and Fuji turned from a sharply defined white cone into a suggestion, and then into nothing.
|
|
||||||
|
|
||||||
The lake surface was perfectly still. The snow fell straight down. There were no other tourists on the path, or if there were I could not see them. It was one of those moments of completely accidental solitude that you cannot plan for and would not trade.
|
|
||||||
|
|
||||||
I sat on a wooden bench on the lakefront for longer than made any meteorological sense. The snow kept falling. A single cormorant sat on a rock offshore and did not move the entire time I was there.
|
|
||||||
|
|
||||||
Caught the bus back to Shinjuku in the afternoon. The mountain never reappeared. I do not mind even slightly.
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
title: 'A Thousand Torii Gates'
|
|
||||||
date: '2026-03-28 11:30'
|
|
||||||
template: entry
|
|
||||||
published: true
|
|
||||||
hero_image: ''
|
|
||||||
lat: '34.9671'
|
|
||||||
lng: '135.7727'
|
|
||||||
location_city: 'Kyoto'
|
|
||||||
location_country: 'Japan'
|
|
||||||
weather_temp_c: 18
|
|
||||||
weather_desc: 'Sunny'
|
|
||||||
---
|
|
||||||
|
|
||||||
The Shinkansen from Tokyo to Kyoto takes two hours and twelve minutes. You travel at 285km/h. At one point Fuji appears out the right-hand window, clear and enormous and completely snow-covered, and the entire carriage rotates slightly to look at it. The mountain is visible for about four minutes. Then it is gone.
|
|
||||||
|
|
||||||
Kyoto is everything Tokyo is not: low, slow, wooden. The streets around Fushimi Inari were already warm with tourists at 11am but the shrine itself is large enough to absorb them. You walk under a tunnel of orange torii gates — thousands of them, each donated by a business and engraved with the donor's name — up a hillside through cedar forest, and the further you climb the more the crowd thins out.
|
|
||||||
|
|
||||||
I walked for two hours. Most visitors turn back at the first lookout. I kept going, past smaller shrines and stone fox statues and mossy steps worn down by a century of feet. Near the top the path was almost empty. The air smelled of pine and incense.
|
|
||||||
|
|
||||||
The city below spread out in all directions. Very few tall buildings — there are strict height regulations to preserve the sightlines. The Kamo River was a thin silver line running south. Distant mountains still wearing snow.
|
|
||||||
|
|
||||||
Dinner at a kaiseki restaurant in Gion, the old entertainment district. Eight small courses, each plated like a small still life. I ate slowly and said nothing and it was the right approach.
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
---
|
|
||||||
title: 'The Deer of Nara'
|
|
||||||
date: '2026-03-29 14:00'
|
|
||||||
template: entry
|
|
||||||
published: true
|
|
||||||
hero_image: ''
|
|
||||||
lat: '34.6851'
|
|
||||||
lng: '135.8048'
|
|
||||||
location_city: 'Nara'
|
|
||||||
location_country: 'Japan'
|
|
||||||
weather_temp_c: 17
|
|
||||||
weather_desc: 'Partly cloudy'
|
|
||||||
---
|
|
||||||
|
|
||||||
The deer at Nara are not afraid of you. This is the first thing you notice — not just that they tolerate humans, but that they regard you with a kind of benign indifference that borders on contempt. They walk into traffic. They push their noses into your pockets. They bow, which sounds enchanting and is, in practice, a manoeuvre to knock crackers out of your hand faster.
|
|
||||||
|
|
||||||
I bought a small bundle of *shika senbei* — deer crackers — from a vendor at the park entrance. They were gone in about forty-five seconds to a small gang of deer who appeared from nowhere and surrounded me in a tight semicircle. One bit my sleeve. Another headbutted a woman walking past who was not even involved.
|
|
||||||
|
|
||||||
Todai-ji temple is at the far end of the park and contains the largest bronze Buddha in Japan. The building is immense — apparently it was rebuilt at two-thirds the original size in the 18th century and is still the largest wooden structure in the world. The Buddha sits in the dim interior looking calm about this. There is a wooden pillar near the back with a hole cut through its base the same width as one of the Buddha's nostrils. Schoolchildren queue to crawl through it. Wisdom awaits on the other side.
|
|
||||||
|
|
||||||
The train back to Kyoto takes 45 minutes through flat agricultural land. The deer do not follow you.
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
---
|
|
||||||
title: 'Dotonbori After Dark'
|
|
||||||
date: '2026-03-30 18:00'
|
|
||||||
template: entry
|
|
||||||
published: true
|
|
||||||
hero_image: ''
|
|
||||||
lat: '34.6687'
|
|
||||||
lng: '135.5017'
|
|
||||||
location_city: 'Osaka'
|
|
||||||
location_country: 'Japan'
|
|
||||||
weather_temp_c: 19
|
|
||||||
weather_desc: 'Cloudy'
|
|
||||||
---
|
|
||||||
|
|
||||||
Osaka is louder than Kyoto and prouder of it. Kyoto has temples and restraint. Osaka has neon and takoyaki and a sign the size of a building advertising a restaurant with a mechanical crab on the front. Both are correct.
|
|
||||||
|
|
||||||
I arrived from Kyoto mid-afternoon, dropped my bag, and went directly to Dotonbori to get my bearings before the evening crowd descended. The canal runs through the entertainment district, and on both sides there are restaurants stacked six floors high with illuminated signs competing for your attention so aggressively that after ten minutes you start to tune out the sensory overload and just walk.
|
|
||||||
|
|
||||||
At six in the evening the neon started properly. The famous running man billboard. The Glico sign. Streets full of people eating while walking — takoyaki (octopus balls, better than they sound), skewered meats, cones of spicy shrimp. Osaka has a word for its own food philosophy: *kuidaore*, which means "eat until you drop."
|
|
||||||
|
|
||||||
I took it as guidance.
|
|
||||||
|
|
||||||
Three hours of eating across four separate establishments. Kushikatsu — battered and deep-fried everything — at a counter in an alley so narrow that diners on opposite sides can shake hands across the table. Soft-serve matcha ice cream on the street. Okonomiyaki from a woman who pressed the pancake flat with a heavy iron tool and would not let me touch anything.
|
|
||||||
|
|
||||||
The canal was dark and the lights were reflected in it and for a while I just stood on the bridge watching people eat.
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
title: 'Seoul Calling'
|
|
||||||
date: '2026-04-01 09:00'
|
|
||||||
template: entry
|
|
||||||
published: true
|
|
||||||
hero_image: ''
|
|
||||||
lat: '37.5635'
|
|
||||||
lng: '126.9851'
|
|
||||||
location_city: 'Seoul'
|
|
||||||
location_country: 'South Korea'
|
|
||||||
weather_temp_c: 10
|
|
||||||
weather_desc: 'Rain'
|
|
||||||
---
|
|
||||||
|
|
||||||
The flight from Osaka to Seoul takes one hour and forty minutes. Shorter than some commutes I have had. At Incheon I changed SIM cards, changed currency, changed alphabet, and walked out into a grey April morning with rain coming in off the Yellow Sea.
|
|
||||||
|
|
||||||
Korea hits differently than Japan. Japan felt deliberate and enclosed, every surface managed, every system timed to the second. Seoul feels faster and more argumentative, as if things are still being decided. The streets around Myeongdong were already busy at 9am: coffee shops the size of ballrooms, street vendors selling *hotteok* (sweet pancakes) from portable griddles, and the particular energy of a city that moves at one speed regardless of the weather.
|
|
||||||
|
|
||||||
My guesthouse is in Mapo-gu, a neighbourhood that turns out to be significantly cooler than anywhere the guidebooks sent me. Independent coffee roasters. Record shops. A gallery in a converted printing house showing black-and-white photography of the Han River in the 1970s.
|
|
||||||
|
|
||||||
I spent the afternoon walking the Han River itself — a massive green ribbon running through the city with dedicated cycling paths, outdoor fitness equipment, and Koreans doing every possible outdoor activity despite the rain. A group of older men playing badminton with very serious expressions. Two people kayaking. A family of five sharing a communal barbecue under an umbrella.
|
|
||||||
|
|
||||||
Dinner: Korean fried chicken at a place that opened at 5pm and was full by 5:05. Beer so cold it was almost painful. Outside, the rain kept up steadily. I stayed longer than I meant to.
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Stories
|
||||||
|
template: stories
|
||||||
|
published: true
|
||||||
|
---
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
title: 'Tuscany Gravel 2025'
|
||||||
|
template: trip
|
||||||
|
date: '2025-09-01'
|
||||||
|
date_start: '2025-09-01'
|
||||||
|
date_end: '2025-09-08'
|
||||||
|
cover_image: ''
|
||||||
|
---
|
||||||
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 140 KiB |
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
title: 'Sorano: Rock and Time'
|
||||||
|
date: '2026-09-03'
|
||||||
|
location_name: Sorano
|
||||||
|
location_country: Italy
|
||||||
|
lat: 42.683
|
||||||
|
lng: 11.715
|
||||||
|
hero_image: hero.jpg
|
||||||
|
hero_alt: Medieval town of Sorano clinging to pale tufa cliffs at dusk
|
||||||
|
published: true
|
||||||
|
---
|
||||||
|
|
||||||
|
The road from Orbetello climbs inland through scrubland and heat. For most of the afternoon there is nothing on the horizon except sky and the occasional electricity pylon. Then, at the top of a ridge, Sorano appears — and the word "appears" does not quite cover it. The town has been carved from a cliff of tufa, a pale volcanic rock so soft you can score it with a fingernail. The buildings are the cliff and the cliff is the buildings.
|
||||||
|
|
||||||
|
[scrolly-section image="hero.jpg" alt="Medieval town of Sorano seen from the approach road, perched on pale tufa cliffs" caption="Sorano — tufa cliff town, Grosseto province"]
|
||||||
|
The approach by bike gives you an unusually long time to study it. The descent into the valley and the climb back up take perhaps forty minutes, and the town is visible for most of that time, doing nothing, requiring nothing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Close up the rock is extraordinary. Hundreds of tomb niches cut into the cliff face — Etruscan graves, most of them open to the sky now, their contents long removed. The people who built this town chose to live surrounded by the evidence of their own mortality. This seems either very brave or very sensible.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
The gate into the old town is fifteenth century and narrow enough that loaded bikes don't fit without turning sideways. Inside, the air is noticeably cooler and the alleys are steep, paved with the same pale tufa, worn smooth by centuries of feet.
|
||||||
|
[/scrolly-section]
|
||||||
|
|
||||||
|
We found a wall to lean the bikes against and sat looking south over the valley we had come from. The light was going amber. Below us, the road we had ridden was already in shadow.
|
||||||
|
|
||||||
|
[chapter-break image="photo-1.jpg" title="After Dark" number="II" alt="Narrow medieval alley in Sorano at dusk, pale stone walls glowing warm" /]
|
||||||
|
|
||||||
|
[pull-quote image="photo-1.jpg" alt="Stone alley in Sorano lit by a single lantern at night"]
|
||||||
|
A town built on rock, carved from rock, returning slowly to rock. Two thousand years of human effort and the cliff remains indifferent.
|
||||||
|
[/pull-quote]
|
||||||
|
|
||||||
|
[scrolly-section image="photo-2.jpg" alt="View south from the tufa cliff walls of Sorano at dusk" caption="Val di Fiora, from the old walls"]
|
||||||
|
One restaurant was open. The menu was four items. We had the pasta with wild boar and the pasta with truffles and a carafe of local wine that cost six euros and was excellent.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
The owner sat at the next table watching a football match on his phone without headphones. Nobody minded. The town outside was completely quiet.
|
||||||
|
[/scrolly-section]
|
||||||
|
|
||||||
|
We were in bed before nine. Sorano at night is absolutely silent. It has been this quiet, in approximately this configuration, for a very long time.
|
||||||
|
After Width: | Height: | Size: 279 KiB |
|
After Width: | Height: | Size: 183 KiB |
|
After Width: | Height: | Size: 290 KiB |
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
title: "Val d'Orcia at Dawn"
|
||||||
|
date: '2026-09-05'
|
||||||
|
location_name: Val d'Orcia
|
||||||
|
location_country: Italy
|
||||||
|
lat: 43.078
|
||||||
|
lng: 11.676
|
||||||
|
hero_image: hero.jpg
|
||||||
|
hero_alt: Wide Tuscan valley at dawn, long cypress shadows across pale gravel road
|
||||||
|
published: true
|
||||||
|
featured: true
|
||||||
|
---
|
||||||
|
|
||||||
|
We left before the heat arrived. The alarm was five-thirty and the sky outside the tent was still more grey than blue. The valley was invisible in the dark except as an absence — a vast silence below us where the shapes of hills ought to be. By six the light had changed. The Val d'Orcia is one of those landscapes that photographers wait years to shoot at this hour, and you can see why: the light arrives at an angle that makes everything look like something from a different century.
|
||||||
|
|
||||||
|
[snap-gallery images="hero.jpg,photo-1.jpg,photo-2.jpg" captions="Six in the morning: the valley belongs entirely to the light,The Cypress Road — every photograph of Tuscany was taken here or somewhere like it,A farmhouse that has been sitting on this hill for four hundred years" alts="Wide misty Tuscan valley at dawn with long shadows,Straight road lined by tall cypress trees in morning light,Stone farmhouse on a hilltop with rolling landscape behind" /]
|
||||||
|
|
||||||
|
The roads down here are white gravel — strade bianche — and the tyres make a particular sound on them that you don't get anywhere else. We rode for two hours without seeing a car. The only other people were two elderly men walking a dog in the opposite direction. They waved.
|
||||||
|
|
||||||
|
[chapter-break image="photo-1.jpg" title="The Hour Before Heat" alt="Cypress road vanishing into a hazy summer morning" /]
|
||||||
|
|
||||||
|
By nine the temperature had already shifted. The quality of the light changed — softer, more diffuse, the sky turning white at the edges. The windows of the farmhouses began to open. Dogs that had been invisible in the dark became visible on walls and in doorways, watching us with professional detachment.
|
||||||
|
|
||||||
|
[snap-gallery images="photo-2.jpg,hero.jpg" captions="The road changes from asphalt to gravel to packed earth and back again without warning,The valley floor at nine: the shadows have shortened, the colours have flattened" alts="Farmhouse detail with terracotta roof and single cypress tree,Tuscan valley road in mid-morning haze" /]
|
||||||
|
|
||||||
|
[pull-quote]
|
||||||
|
The best hours of a cycling day are the ones nobody else sees. Before the heat arrives, before the cafes open, before the traffic comes. Everything belongs to you then.
|
||||||
|
[/pull-quote]
|
||||||
|
|
||||||
|
We reached Pienza at eleven-thirty. The ice-cream queue was eight deep and entirely justified.
|
||||||
|
After Width: | Height: | Size: 419 KiB |
|
After Width: | Height: | Size: 161 KiB |
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
title: 'One Evening in Siena'
|
||||||
|
date: '2026-09-05'
|
||||||
|
location_name: Siena
|
||||||
|
location_country: Italy
|
||||||
|
lat: 43.318
|
||||||
|
lng: 11.330
|
||||||
|
hero_image: hero.jpg
|
||||||
|
hero_alt: Piazza del Campo at dusk, terracotta paving fading from gold to shadow
|
||||||
|
published: true
|
||||||
|
---
|
||||||
|
|
||||||
|
[pull-quote image="hero.jpg" alt="Piazza del Campo seen from the upper rim at golden hour"]
|
||||||
|
Siena is not a city that tries to impress you. It has been here for a thousand years and intends to be here for a thousand more. You fit around it, not the other way.
|
||||||
|
[/pull-quote]
|
||||||
|
|
||||||
|
We rolled in at half past six, legs finished, panniers heavier than they started. The Campo appeared without warning at the end of a narrow street and we both stopped pedalling at exactly the same moment. That particular square does something to people. It is partly the shape — a shallow bowl, a scallop shell, the way it holds you — and partly the light at that hour, which turns the terracotta pavement the colour of old copper.
|
||||||
|
|
||||||
|
[chapter-break image="photo-1.jpg" title="The Campo" number="I" alt="Detail of Siena's herringbone brick pavement catching the last light" /]
|
||||||
|
|
||||||
|
[scrolly-section image="hero.jpg" alt="Piazza del Campo filling with people as evening comes" caption="Campo, 19:00 — the square fills from the edges inward"]
|
||||||
|
The locals arrive first. They know which spot faces west and which benches stay in the shade longest. Then the tourists, then the pigeons, then the long shadows.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
A busker with an accordion near the Fonte Gaia. A group of students lying on the slope reading. Three children running in a circle for reasons nobody questioned.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
We sat on the pavement with our backs against the warm brickwork of the Palazzo Pubblico and did not move for forty minutes. The relief of sitting still after eight hours on a bike is a specific physical sensation. It travels upward from your legs and settles somewhere just behind the sternum.
|
||||||
|
[/scrolly-section]
|
||||||
|
|
||||||
|
We found a place for dinner three streets away, down a flight of steps with no sign outside. The pasta was handmade, the wine was local, the bill was reasonable. We were in bed by ten. Tomorrow: Florence.
|
||||||
|
After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 74 KiB |
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
title: 'Florence Without a Map'
|
||||||
|
date: '2026-09-07'
|
||||||
|
location_name: Florence
|
||||||
|
location_country: Italy
|
||||||
|
lat: 43.769
|
||||||
|
lng: 11.255
|
||||||
|
hero_image: hero.jpg
|
||||||
|
hero_alt: Arno river at midday with Ponte Vecchio, ochre buildings reflected in still water
|
||||||
|
published: true
|
||||||
|
---
|
||||||
|
|
||||||
|
No route today. No GPS, no distance target, no reason to be anywhere by any particular time. After six days of forward motion this felt almost wrong — the instinct to check the elevation profile arriving at nothing. We put the bikes in the hotel basement and walked out into Florence on foot.
|
||||||
|
|
||||||
|
[chapter-break image="hero.jpg" title="Day Seven" number="VII" alt="Arno river and Ponte Vecchio from Ponte Santa Trinita at midday" /]
|
||||||
|
|
||||||
|
[snap-gallery images="hero.jpg,photo-1.jpg" captions="The Arno at noon — greener than expected, the bridges older than you remember,Via dei Servi: washing lines, shutters, a cat on a warm stone ledge that had been warm since morning" alts="Arno river with Ponte Vecchio reflected in still water at midday,Narrow Florence street with laundry strung between buildings" /]
|
||||||
|
|
||||||
|
[pull-quote]
|
||||||
|
Cycling makes you earn every city you arrive at. Florence, we got for free. It felt like a gift and a debt simultaneously.
|
||||||
|
[/pull-quote]
|
||||||
|
|
||||||
|
[scrolly-section image="photo-1.jpg" alt="Narrow Oltrarno street in afternoon light" caption="Oltrarno, 14:00"]
|
||||||
|
The Uffizi had a queue that stretched around two corners and disappeared into a side street. We looked at it for a moment and went to find coffee instead. This felt correct.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
A covered market in the Oltrarno that nobody had told us about. A man selling leather goods from a table he clearly reassembled each morning from identical components. A small dog sleeping under a fruit stall in a precisely calculated patch of shade.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
We crossed the Ponte Vecchio at two in the afternoon, which is exactly the wrong time to cross the Ponte Vecchio, and it was still worth it. The light off the Arno at that hour is genuinely extraordinary and all the photographs in the world do not prepare you for it.
|
||||||
|
[/scrolly-section]
|
||||||
|
|
||||||
|
Dinner near the apartment, early. Feet sore in a different way from legs sore — a smaller, more concentrated complaint. Tomorrow: the last day. The coast road home.
|
||||||
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 44 KiB |
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: 'Setting Off from Campiglia'
|
||||||
|
date: '2026-09-01 07:00'
|
||||||
|
template: entry
|
||||||
|
published: true
|
||||||
|
featured: true
|
||||||
|
hero_image: ''
|
||||||
|
lat: 43.024
|
||||||
|
lng: 10.603
|
||||||
|
location_city: Campiglia Marittima
|
||||||
|
location_country: Italy
|
||||||
|
weather_temp_c: 27
|
||||||
|
weather_desc: Sunny
|
||||||
|
---
|
||||||
|
|
||||||
|
Seven in the morning and the coast road is still cool. We loaded the bikes in the car park below the old town, the panniers heavier than they should be and the weather forecast saying nine consecutive days of sun. The route heads south first — down into the Maremma, then east, then a long loop back. Eight days. Nobody goes this way in September except cyclists and people who have got lost.
|
||||||
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 250 KiB |
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
title: 'Maremma in Full Sun'
|
||||||
|
date: '2026-09-02 11:30'
|
||||||
|
template: entry
|
||||||
|
published: true
|
||||||
|
hero_image: ''
|
||||||
|
lat: 42.612
|
||||||
|
lng: 11.171
|
||||||
|
location_city: Maremma
|
||||||
|
location_country: Italy
|
||||||
|
weather_temp_c: 29
|
||||||
|
weather_desc: Sunny
|
||||||
|
---
|
||||||
|
|
||||||
|
Eleven-thirty and already thirty degrees. The Maremma is agricultural land and scrubland and very little else, and in September it has the quality of a landscape that has given up trying. The road is straight, the sun is direct, the shadows are almost vertical. We stopped at a petrol station and drank two cans of something cold each. The man at the counter looked at us like people who had made a series of questionable decisions.
|
||||||
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 166 KiB |
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
title: 'The Lagoon at Dusk'
|
||||||
|
date: '2026-09-02 19:00'
|
||||||
|
template: entry
|
||||||
|
published: true
|
||||||
|
hero_image: ''
|
||||||
|
lat: 42.442
|
||||||
|
lng: 11.218
|
||||||
|
location_city: Orbetello
|
||||||
|
location_country: Italy
|
||||||
|
weather_temp_c: 24
|
||||||
|
weather_desc: Partly cloudy
|
||||||
|
---
|
||||||
|
|
||||||
|
Orbetello sits on a causeway between two lagoons and at dusk the light does something remarkable to the water. Pink flamingos — real ones, not ornamental — were standing in the shallows on the western side, perfectly still. We ate at a table outside overlooking the eastern lagoon. The sky turned orange and then purple and then a deep blue that was almost indistinguishable from the water. The wine was cold and the pasta had clams.
|
||||||
|
After Width: | Height: | Size: 101 KiB |
|
After Width: | Height: | Size: 150 KiB |
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
title: 'Orbetello Morning'
|
||||||
|
date: '2026-09-03 08:00'
|
||||||
|
template: entry
|
||||||
|
published: true
|
||||||
|
hero_image: ''
|
||||||
|
lat: 42.442
|
||||||
|
lng: 11.217
|
||||||
|
location_city: Orbetello
|
||||||
|
location_country: Italy
|
||||||
|
weather_temp_c: 22
|
||||||
|
weather_desc: Sunny
|
||||||
|
---
|
||||||
|
|
||||||
|
The lagoon at eight in the morning is a different thing from the lagoon at eight in the evening. Flat, silver, nearly silent. A single fisherman in a small boat about two hundred metres out, not appearing to fish. We left before the town had properly woken up, heading northeast on roads that climbed immediately and steeply into a landscape of oak and limestone that felt nothing like the coast we had left behind twenty minutes before.
|
||||||
|
After Width: | Height: | Size: 102 KiB |
|
After Width: | Height: | Size: 50 KiB |
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
title: 'Tufa and Towers'
|
||||||
|
date: '2026-09-03 17:00'
|
||||||
|
template: entry
|
||||||
|
published: true
|
||||||
|
hero_image: ''
|
||||||
|
lat: 42.683
|
||||||
|
lng: 11.715
|
||||||
|
location_city: Sorano
|
||||||
|
location_country: Italy
|
||||||
|
weather_temp_c: 26
|
||||||
|
weather_desc: Sunny
|
||||||
|
---
|
||||||
|
|
||||||
|
Sorano appears on the horizon an hour before you reach it: a cluster of towers and walls on a pale cliff, floating above the valley. The closer you get the stranger it becomes. The town is not built on rock — the town is rock, volcanic tufa carved and inhabited over two thousand years. The Etruscans started it. Everyone since has just kept adding floors. We are staying the night and it already feels like somewhere that requires more time than we have.
|
||||||
|
After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 60 KiB |
|
After Width: | Height: | Size: 142 KiB |
|
After Width: | Height: | Size: 107 KiB |
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
title: 'The Long Climb North'
|
||||||
|
date: '2026-09-04 15:00'
|
||||||
|
template: entry
|
||||||
|
published: true
|
||||||
|
hero_image: ''
|
||||||
|
lat: 43.077
|
||||||
|
lng: 11.678
|
||||||
|
location_city: "Val d'Orcia"
|
||||||
|
location_country: Italy
|
||||||
|
weather_temp_c: 23
|
||||||
|
weather_desc: Partly cloudy
|
||||||
|
---
|
||||||
|
|
||||||
|
Today was the hardest day. The route from Sorano to the Val d'Orcia crosses the eastern slope of Monte Amiata, which sounds manageable on a map and is not manageable at all. By noon we had climbed eleven hundred metres. By two we were somewhere above Seggiano in thin cloud, the views long gone, legs complaining in a language that had become very specific. Then the cloud lifted and the Val d'Orcia was simply there below us: pale roads, dark cypress, the whole thing exactly as advertised. Sometimes the landscapes that have been photographed to death are still worth arriving at.
|
||||||
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 49 KiB |
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
title: 'Before the Heat Arrives'
|
||||||
|
date: '2026-09-05 08:30'
|
||||||
|
template: entry
|
||||||
|
published: true
|
||||||
|
hero_image: ''
|
||||||
|
lat: 43.078
|
||||||
|
lng: 11.676
|
||||||
|
location_city: Pienza
|
||||||
|
location_country: Italy
|
||||||
|
weather_temp_c: 21
|
||||||
|
weather_desc: Sunny
|
||||||
|
---
|
||||||
|
|
||||||
|
Six o'clock and the valley below Pienza is still in shadow. We left camp early on purpose — the route to Siena is long and September sun waits for no one. On the strade bianche the tyres make a sound like distant applause. No cars for the first two hours. Just the road and the light doing things to the cypress trees that would be embarrassing to describe in any other context.
|
||||||
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 191 KiB |
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
title: 'Into Siena'
|
||||||
|
date: '2026-09-05 18:00'
|
||||||
|
template: entry
|
||||||
|
published: true
|
||||||
|
hero_image: ''
|
||||||
|
lat: 43.318
|
||||||
|
lng: 11.335
|
||||||
|
location_city: Siena
|
||||||
|
location_country: Italy
|
||||||
|
weather_temp_c: 25
|
||||||
|
weather_desc: Sunny
|
||||||
|
---
|
||||||
|
|
||||||
|
The approach to Siena by bike is through streets that get progressively older and steeper until suddenly the Campo is there. We had both seen it in photographs and the photographs are accurate in every way except one: they do not tell you how the square smells — stone and frying onions and the particular warm stillness of a Sienese summer evening. We sat on the pavement with our backs against the Palazzo Pubblico for forty minutes and did not want to be anywhere else.
|
||||||
|
After Width: | Height: | Size: 158 KiB |
|
After Width: | Height: | Size: 99 KiB |
|
After Width: | Height: | Size: 170 KiB |
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
title: 'Florence by Nightfall'
|
||||||
|
date: '2026-09-06 20:00'
|
||||||
|
template: entry
|
||||||
|
published: true
|
||||||
|
hero_image: ''
|
||||||
|
lat: 43.767
|
||||||
|
lng: 11.253
|
||||||
|
location_city: Florence
|
||||||
|
location_country: Italy
|
||||||
|
weather_temp_c: 21
|
||||||
|
weather_desc: Cloudy
|
||||||
|
---
|
||||||
|
|
||||||
|
A long day. Siena to Florence is ninety kilometres and involves two significant climbs before you reach the Chianti hills, after which it becomes more manageable but you have already used the legs you needed. We came in from the south as the light was going, the city materialising from a distance as a density of rooftops and towers. The Arno appeared between buildings and we crossed it and then we were in, which is always a slightly surprising moment after a long day.
|
||||||
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 144 KiB |
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
title: 'One Rest Day'
|
||||||
|
date: '2026-09-07 14:00'
|
||||||
|
template: entry
|
||||||
|
published: true
|
||||||
|
hero_image: ''
|
||||||
|
lat: 43.769
|
||||||
|
lng: 11.255
|
||||||
|
location_city: Florence
|
||||||
|
location_country: Italy
|
||||||
|
weather_temp_c: 22
|
||||||
|
weather_desc: Partly cloudy
|
||||||
|
---
|
||||||
|
|
||||||
|
The bikes stayed in the basement. We walked instead, which after six days of cycling felt simultaneously easier and harder — easier on the legs, harder on the feet, which are used to being passive. Florence does not require a plan. Every street contains something. We crossed the Arno four times from different bridges, each one giving a slightly different version of the same view, all of them good.
|
||||||
|
After Width: | Height: | Size: 110 KiB |
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
title: 'Dawn on the Cecina Coast'
|
||||||
|
date: '2026-09-08 07:30'
|
||||||
|
template: entry
|
||||||
|
published: true
|
||||||
|
hero_image: ''
|
||||||
|
lat: 43.553
|
||||||
|
lng: 10.313
|
||||||
|
location_city: Cecina
|
||||||
|
location_country: Italy
|
||||||
|
weather_temp_c: 20
|
||||||
|
weather_desc: Sunny
|
||||||
|
---
|
||||||
|
|
||||||
|
The last day starts on the coast road south of Cecina, the sea visible between the pine trees. We have been inland for most of the week and the smell of salt water is a surprise. The road is flat, which after eight days of Tuscan hills feels almost suspicious. We rode in silence for the first hour. There was nothing that needed saying.
|
||||||
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 54 KiB |
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
title: 'Home'
|
||||||
|
date: '2026-09-08 16:30'
|
||||||
|
template: entry
|
||||||
|
published: true
|
||||||
|
hero_image: ''
|
||||||
|
lat: 43.017
|
||||||
|
lng: 10.587
|
||||||
|
location_city: Campiglia Marittima
|
||||||
|
location_country: Italy
|
||||||
|
weather_temp_c: 26
|
||||||
|
weather_desc: Sunny
|
||||||
|
---
|
||||||
|
|
||||||
|
The old town of Campiglia was visible on its hill for the last twenty kilometres, appearing and disappearing between the trees the way it had appeared on the horizon eight days ago when we left. The loop is complete: same car park, same view across the coast, different legs. The bikes went back in the car and we sat on a wall and counted the countries and the kilometres and the pasta dishes. Eight days, one loop, Tuscany in September. It was exactly what it was supposed to be.
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
title: Journal
|
||||||
|
template: dailies
|
||||||
|
content:
|
||||||
|
items: '@self.children'
|
||||||
|
order:
|
||||||
|
by: date
|
||||||
|
dir: desc
|
||||||
|
filter:
|
||||||
|
published: true
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 'Trip Map'
|
||||||
|
template: map
|
||||||
|
---
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 'Trip Stats'
|
||||||
|
template: stats
|
||||||
|
---
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: Stories
|
||||||
|
template: stories
|
||||||
|
published: true
|
||||||
|
---
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
title: 'Tuscany 2026'
|
||||||
|
template: trip
|
||||||
|
date: '2026-09-01'
|
||||||
|
date_start: '2026-09-01'
|
||||||
|
date_end: '2026-09-08'
|
||||||
|
cover_image: ''
|
||||||
|
---
|
||||||
@@ -1,368 +0,0 @@
|
|||||||
# Into the East — Design Spec
|
|
||||||
|
|
||||||
**Date:** 2026-06-18
|
|
||||||
**Status:** Approved for implementation
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. Direction
|
|
||||||
|
|
||||||
**The brief:** A personal travel journal, sole author, trip to East Asia. Three weeks to implement before departure. Audience is both friends/family and the occasional curious stranger.
|
|
||||||
|
|
||||||
**The position:** Neither Polarsteps nor FindPenguins. Both optimize for social sharing of travel data. This site optimizes for **the story** — and should feel like reading a well-edited travel journal, not using an app.
|
|
||||||
|
|
||||||
**What we steal from each:**
|
|
||||||
- Polarsteps: photography-first hierarchy, airy whitespace, map as the emotional spine of the trip
|
|
||||||
- FindPenguins: typography as brand identity, stats as trophy case, hierarchical trip → entry structure
|
|
||||||
|
|
||||||
**What we do better than both:**
|
|
||||||
- Web-native: fast, linkable, no install, works on any browser
|
|
||||||
- Single author = pure editorial voice, no social noise
|
|
||||||
- Full CSS control = real typographic identity, not generic app chrome
|
|
||||||
- Editorial feel: more travel magazine, less productivity dashboard
|
|
||||||
|
|
||||||
**Aesthetic direction:** Field notes. The kind of journal a thoughtful traveler would carry — clean, direct, lets the photography speak. Sophisticated without effort.
|
|
||||||
|
|
||||||
**The one aesthetic risk:** Full-bleed hero photography with a translucent date+location overlay at the bottom of each card. The photo IS the entry card — not a thumbnail beside text. This is the single element that distinguishes this design from both reference apps and from typical blog layouts.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Color System
|
|
||||||
|
|
||||||
### Palette
|
|
||||||
|
|
||||||
| Token | Hex | Usage |
|
|
||||||
|---|---|---|
|
|
||||||
| `--color-ink` | `#17171A` | Primary text (near-black with cool undertone, like ink) |
|
|
||||||
| `--color-ink-2` | `#4A4850` | Secondary text, body paragraphs |
|
|
||||||
| `--color-ink-muted` | `#9896A0` | Labels, timestamps, captions, placeholder text |
|
|
||||||
| `--color-paper` | `#F7F5F2` | Page background (warm paper white, not blue-white) |
|
|
||||||
| `--color-canvas` | `#FFFFFF` | Card backgrounds, modals, form surfaces |
|
|
||||||
| `--color-border` | `#E8E6E3` | Standard dividers, card borders |
|
|
||||||
| `--color-border-soft` | `#F0EDEA` | Subtle section dividers |
|
|
||||||
| `--color-accent` | `#1F6B5A` | Deep teal — brand color, links, CTAs, active states |
|
|
||||||
| `--color-accent-hover` | `#185647` | Darkened accent for hover/pressed states |
|
|
||||||
| `--color-accent-light` | `#EBF5F2` | Pale teal for highlight backgrounds |
|
|
||||||
| `--color-accent-on` | `#FFFFFF` | Text on accent-colored surfaces |
|
|
||||||
|
|
||||||
### Rationale for accent color
|
|
||||||
|
|
||||||
Deep teal `#1F6B5A` was chosen over:
|
|
||||||
- Blue (#0066cc current): too generic, too tech
|
|
||||||
- Orange/saffron: clichéd for "Asia" travel design
|
|
||||||
- Terracotta/cream: the most common default for lifestyle/travel blogs
|
|
||||||
|
|
||||||
Teal evokes bamboo, celadon porcelain, ancient jade, the color of temple gardens — all without being literal or kitsch. It works cleanly against both the warm paper background and white card surfaces.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Typography
|
|
||||||
|
|
||||||
### Fonts
|
|
||||||
|
|
||||||
| Role | Family | Fallback | Source |
|
|
||||||
|---|---|---|---|
|
|
||||||
| Display / Headings | DM Serif Display | Georgia, serif | Google Fonts |
|
|
||||||
| UI / Body / Labels | DM Sans | -apple-system, BlinkMacSystemFont, sans-serif | Google Fonts |
|
|
||||||
|
|
||||||
**Google Fonts URL:**
|
|
||||||
```
|
|
||||||
https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,400&family=DM+Serif+Display:ital@0;1&display=swap
|
|
||||||
```
|
|
||||||
|
|
||||||
**Why this pairing:**
|
|
||||||
DM Serif Display has a calligraphic quality — slightly editorial, authoritative but not stiff. Paired with DM Sans (its designed companion) the system is cohesive. DM Sans is neutral and highly legible at all sizes. Both are under-used relative to Inter/Lato/Playfair, so the combination has a distinctive voice without being trendy.
|
|
||||||
|
|
||||||
### Type Scale
|
|
||||||
|
|
||||||
| Token | Size | Line Height | Usage |
|
|
||||||
|---|---|---|---|
|
|
||||||
| `--text-xs` | 0.75rem (12px) | 1.5 | Badges, captions |
|
|
||||||
| `--text-sm` | 0.875rem (14px) | 1.5 | Meta, timestamps, labels |
|
|
||||||
| `--text-base` | 1rem (16px) | 1.65 | Body paragraphs |
|
|
||||||
| `--text-md` | 1.125rem (18px) | 1.55 | Lead text, intro paragraphs |
|
|
||||||
| `--text-lg` | 1.375rem (22px) | 1.35 | Subheadings, card titles (mobile) |
|
|
||||||
| `--text-xl` | 1.75rem (28px) | 1.25 | Entry card titles |
|
|
||||||
| `--text-2xl` | 2.25rem (36px) | 1.2 | Page headings, entry titles (desktop) |
|
|
||||||
| `--text-3xl` | 3rem (48px) | 1.1 | Hero entry title |
|
|
||||||
|
|
||||||
### Usage rules
|
|
||||||
|
|
||||||
- Entry titles: `--font-display`, `--text-xl` (mobile) / `--text-2xl` (desktop)
|
|
||||||
- Site title in header: `--font-display`, `--text-lg`
|
|
||||||
- All other UI text: `--font-ui`
|
|
||||||
- Body paragraphs: `--font-ui`, `--text-base`, `--leading-normal`
|
|
||||||
- Timestamps/badges: `--font-ui`, `--text-xs`, uppercase, `letter-spacing: 0.07em`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Spacing & Layout
|
|
||||||
|
|
||||||
### Spacing scale (4px base unit)
|
|
||||||
|
|
||||||
| Token | Value |
|
|
||||||
|---|---|
|
|
||||||
| `--space-1` | 0.25rem (4px) |
|
|
||||||
| `--space-2` | 0.5rem (8px) |
|
|
||||||
| `--space-3` | 0.75rem (12px) |
|
|
||||||
| `--space-4` | 1rem (16px) |
|
|
||||||
| `--space-5` | 1.25rem (20px) |
|
|
||||||
| `--space-6` | 1.5rem (24px) |
|
|
||||||
| `--space-8` | 2rem (32px) |
|
|
||||||
| `--space-10` | 2.5rem (40px) |
|
|
||||||
| `--space-12` | 3rem (48px) |
|
|
||||||
| `--space-16` | 4rem (64px) |
|
|
||||||
|
|
||||||
### Layout
|
|
||||||
|
|
||||||
- Content max-width: `720px` (comfortable reading at any font size)
|
|
||||||
- Page horizontal padding: `1.25rem` (mobile), `1.5rem` (desktop ≥520px)
|
|
||||||
- Header height: `60px` (fixed, for JS offset calculations)
|
|
||||||
- Map page: full viewport, no content max-width constraint
|
|
||||||
|
|
||||||
### Border radius
|
|
||||||
|
|
||||||
| Token | Value | Usage |
|
|
||||||
|---|---|---|
|
|
||||||
| `--radius-sm` | 4px | Photo corners, small chips |
|
|
||||||
| `--radius-md` | 8px | Cards, buttons, inputs |
|
|
||||||
| `--radius-lg` | 12px | Large cards, modals |
|
|
||||||
| `--radius-full` | 9999px | Pills, badges |
|
|
||||||
|
|
||||||
### Shadows
|
|
||||||
|
|
||||||
| Token | Value | Usage |
|
|
||||||
|---|---|---|
|
|
||||||
| `--shadow-sm` | `0 1px 3px rgba(0,0,0,0.08)` | Stat blocks, subtle elevation |
|
|
||||||
| `--shadow-md` | `0 4px 12px rgba(0,0,0,0.10)` | Cards on hover, dropdowns |
|
|
||||||
| `--shadow-lg` | `0 8px 24px rgba(0,0,0,0.14)` | Lightbox, modals |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. Component Inventory
|
|
||||||
|
|
||||||
### 5.1 Site Header
|
|
||||||
|
|
||||||
```
|
|
||||||
[ into the east ] [ Journal Map Stats ]
|
|
||||||
← accent bar across top (3px) ───────────────────────────────
|
|
||||||
```
|
|
||||||
|
|
||||||
- Top border: `3px solid var(--color-accent)` — thin accent bar signals the brand color without decorating
|
|
||||||
- Site title: DM Serif Display, `--text-lg`, no decoration
|
|
||||||
- Nav links: DM Sans, `--text-sm`, weight 500, `--color-ink-2`
|
|
||||||
- Active nav link: `--color-accent`, weight 600
|
|
||||||
- Mobile: same layout, title slightly smaller, nav links compact
|
|
||||||
- Background: `--color-canvas` (white), bottom border `1px solid var(--color-border)`
|
|
||||||
|
|
||||||
### 5.2 Entry Feed Card — With Photo
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────┐
|
|
||||||
│ │
|
|
||||||
│ [photo] │ ← full-width, 16:9, rounded corners
|
|
||||||
│ │
|
|
||||||
│ 18 JUN · 📍 Kyoto, Japan │ ← overlaid at bottom, gradient mask
|
|
||||||
└─────────────────────────────────────┘
|
|
||||||
Arrived in Tokyo ← DM Serif Display, --text-xl
|
|
||||||
After 14 hours of flying I finally ← body excerpt, --color-ink-2
|
|
||||||
set foot on Japanese soil...
|
|
||||||
Read entry → ← --color-accent, --text-sm
|
|
||||||
```
|
|
||||||
|
|
||||||
- Photo: `aspect-ratio: 16/9`, `object-fit: cover`, `border-radius: var(--radius-md)`
|
|
||||||
- Photo has a `linear-gradient(to top, rgba(0,0,0,0.55), transparent)` overlay at the bottom 40%
|
|
||||||
- Date + location sit on top of gradient in white text (`rgba(255,255,255,0.92)`)
|
|
||||||
- On hover: photo scales to 1.03 (subtle zoom, 0.4s ease)
|
|
||||||
- Title below photo: DM Serif Display, hover turns `--color-accent`
|
|
||||||
- Card separation: `padding-bottom: var(--space-12)` + `border-bottom: 1px solid var(--color-border)`
|
|
||||||
|
|
||||||
### 5.3 Entry Feed Card — No Photo
|
|
||||||
|
|
||||||
When no photo is available, fall back to a text-only layout:
|
|
||||||
|
|
||||||
```
|
|
||||||
18 JUN 2026 · 📍 Kyoto, Japan ← meta row, --text-sm, --color-ink-muted
|
|
||||||
|
|
||||||
Arrived in Tokyo ← DM Serif Display, --text-xl
|
|
||||||
After 14 hours of flying...
|
|
||||||
Read entry →
|
|
||||||
```
|
|
||||||
|
|
||||||
- No photo container
|
|
||||||
- Meta (date + location) on one line above title, small + muted
|
|
||||||
|
|
||||||
### 5.4 Single Entry Page
|
|
||||||
|
|
||||||
```
|
|
||||||
Wednesday, 18 June 2026 ← --text-sm, --color-ink-muted, uppercase
|
|
||||||
📍 Kyoto, Japan · ⛅ Partly cloudy · 22°C
|
|
||||||
|
|
||||||
Arrived in Tokyo ← DM Serif Display, --text-2xl / --text-3xl
|
|
||||||
─────────────────────────────────────
|
|
||||||
Body text content... ← --font-ui, --text-base/md
|
|
||||||
|
|
||||||
[Photo gallery — 2 or 3 col grid]
|
|
||||||
|
|
||||||
← Back to journal
|
|
||||||
```
|
|
||||||
|
|
||||||
- The entry title uses `--font-display` at largest scale
|
|
||||||
- A thin `--color-border` rule separates the header from the body
|
|
||||||
- Body text is `--text-md` (18px) for comfortable long-form reading
|
|
||||||
- Full-bleed hero option: if a `hero_image` is set, it spans the full content width with a bottom margin
|
|
||||||
|
|
||||||
### 5.5 Post Form (Author View)
|
|
||||||
|
|
||||||
```
|
|
||||||
New Entry
|
|
||||||
|
|
||||||
Title * [________________________]
|
|
||||||
Date & Time [2026-06-18 14:30 ]
|
|
||||||
What happened [ ]
|
|
||||||
today? [ ]
|
|
||||||
[ ]
|
|
||||||
|
|
||||||
Photos [ + Add photos (max 4) ]
|
|
||||||
|
|
||||||
City [________________________]
|
|
||||||
Country [________________________]
|
|
||||||
|
|
||||||
[ 📍 Get Location ] [ 🌤 Get Weather ]
|
|
||||||
✓ Location captured: Kyoto, Japan ← status line
|
|
||||||
|
|
||||||
[ Post Entry ]
|
|
||||||
```
|
|
||||||
|
|
||||||
UX changes from current:
|
|
||||||
- Lat/lng inputs **hidden from the UI** (remain in the form as `display:none` for data capture, filled by JS)
|
|
||||||
- Location status shows captured city/country + coordinates in a single line (not separate status paragraphs)
|
|
||||||
- Photo upload area: larger touch target, visual indication of count
|
|
||||||
- "Post Entry" button: `--color-accent` background, full-width on mobile, `min-height: 52px`
|
|
||||||
- Form fields: `--radius-md` corners, `--color-border` border, focus ring in `--color-accent`
|
|
||||||
- Section spacing: generous vertical rhythm on mobile
|
|
||||||
|
|
||||||
### 5.6 Stats Page
|
|
||||||
|
|
||||||
```
|
|
||||||
┌────────────┐ ┌────────────┐
|
|
||||||
│ 42 │ │ 18 │
|
|
||||||
│ days on │ │ entries │
|
|
||||||
│ the road │ │ posted │
|
|
||||||
└────────────┘ └────────────┘
|
|
||||||
┌────────────┐ ┌────────────┐
|
|
||||||
│ 6 │ │ ~14,200 │
|
|
||||||
│ countries │ │ km │
|
|
||||||
│ visited │ │ traveled │
|
|
||||||
└────────────┘ └────────────┘
|
|
||||||
|
|
||||||
Countries visited
|
|
||||||
Japan · South Korea · Mongolia · Russia · Finland · Estonia
|
|
||||||
```
|
|
||||||
|
|
||||||
- Numbers: `--font-display`, `--text-3xl`, `--color-accent`
|
|
||||||
- Labels: `--font-ui`, `--text-xs`, uppercase, `--color-ink-muted`
|
|
||||||
- Cards: white, `--shadow-sm`, `--radius-md`, centered
|
|
||||||
|
|
||||||
### 5.7 Map Page
|
|
||||||
|
|
||||||
Minimal changes — the map itself is good. Style improvements:
|
|
||||||
- Leaflet popups: match the new design (DM Sans, `--radius-md`, `--shadow-md`)
|
|
||||||
- Markers: keep current circle style, update color to `--color-accent`
|
|
||||||
- Feed mini-map wrapper: match `--radius-md`, `--border`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. UX Flows
|
|
||||||
|
|
||||||
### 6.1 Reader — First Visit
|
|
||||||
|
|
||||||
1. Land on `/tracker` (journal feed)
|
|
||||||
2. See mini-map above fold (if entries exist) — route tells the geographic story at a glance
|
|
||||||
3. First entry card: full-bleed hero photo with date/location overlay — immediate emotional pull
|
|
||||||
4. Scroll through chronological entries
|
|
||||||
5. Tap/click entry → entry detail page
|
|
||||||
6. Navigate back via "← Back to journal"
|
|
||||||
|
|
||||||
**Key principle:** The reader should understand the journey spatially (mini-map) and emotionally (hero photo) before reading a single word.
|
|
||||||
|
|
||||||
### 6.2 Reader — Navigation
|
|
||||||
|
|
||||||
- Journal: primary destination, the feed
|
|
||||||
- Map: geographic exploration mode
|
|
||||||
- Stats: quick numbers, satisfying progress indicator
|
|
||||||
- No account required, no social friction, no login prompt for readers
|
|
||||||
|
|
||||||
### 6.3 Author — Posting from Mobile
|
|
||||||
|
|
||||||
1. Navigate to `/post` (bookmark on home screen)
|
|
||||||
2. Already logged in (Grav session persists) — form loads directly
|
|
||||||
3. **Title**: tap → type (autofocused)
|
|
||||||
4. **Date & Time**: auto-filled to now, adjust if needed
|
|
||||||
5. **Content**: write what happened
|
|
||||||
6. **Photos**: tap "Add photos" → camera or gallery → select up to 4
|
|
||||||
7. **Location**: tap "📍 Get Location" → GPS fires → status shows "Kyoto, Japan · 34.985, 135.758" in one line
|
|
||||||
8. **Weather**: tap "🌤 Get Weather" (works only if location was captured) → status shows "Partly cloudy · 22°C"
|
|
||||||
9. **City/Country**: auto-populated from GPS is a nice-to-have for v2; in v1 type manually if needed
|
|
||||||
10. Tap "Post Entry" → success message → 2-second pause → redirect to /tracker (new entry visible at top)
|
|
||||||
|
|
||||||
**Key principles:**
|
|
||||||
- One-thumb operation for all critical actions on mobile
|
|
||||||
- Location/weather are conveniences, not blockers — can skip both
|
|
||||||
- Visual feedback is immediate (status line updates on GPS response)
|
|
||||||
- After submit: don't leave author on a success message page; redirect to see their new post
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. Mobile Specifics
|
|
||||||
|
|
||||||
### Touch targets
|
|
||||||
- All interactive elements: `min-height: 44px`, `min-width: 44px` (Apple HIG standard)
|
|
||||||
- Form buttons: `min-height: 52px` on the post form (primary CTA)
|
|
||||||
- Nav links: `padding: 0.5rem 0.75rem`
|
|
||||||
|
|
||||||
### Viewport concerns
|
|
||||||
- Map page: `height: calc(100vh - 60px)`, `touch-action: none` on map container — prevents scroll trap
|
|
||||||
- Photo lightbox: full viewport overlay, swipe-friendly (keyboard + click already implemented)
|
|
||||||
- Form on mobile: single-column, generous input padding `0.875rem 1rem`, `font-size: 1rem` (prevents iOS zoom on focus)
|
|
||||||
|
|
||||||
### Performance
|
|
||||||
- Google Fonts: loaded with `preconnect` hints
|
|
||||||
- Images: `loading="lazy"` on all non-above-fold images (already in place)
|
|
||||||
- Leaflet: loaded from CDN, only on pages that need it
|
|
||||||
- No new JS frameworks — vanilla JS throughout
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. Tech Stack Decision
|
|
||||||
|
|
||||||
**Keep Grav CMS.** With a 3-week timeline, replacing it would consume all available time on migration rather than design improvements.
|
|
||||||
|
|
||||||
| Layer | Decision | Rationale |
|
|
||||||
|---|---|---|
|
|
||||||
| Backend | Grav CMS (PHP, Twig) — unchanged | Works, flat-file, no DB |
|
|
||||||
| CSS | Vanilla CSS + custom properties (design tokens) | No build step, full control, ships as one file |
|
|
||||||
| JS | Vanilla JS — unchanged | Current JS is well-structured, scope doesn't justify a framework |
|
|
||||||
| Icons | Unicode + emoji (current) | No dependency, works everywhere |
|
|
||||||
| Fonts | Google Fonts via CDN | Two fonts, display-swap, negligible impact |
|
|
||||||
| Maps | Leaflet.js (current) | Already in use, no reason to change |
|
|
||||||
| Build | None — no build pipeline | Grav's asset pipeline handles minification if needed |
|
|
||||||
|
|
||||||
**No Alpine.js, no TypeScript, no Tailwind.** The site has clean vanilla JS and CSS today; a redesign is about visual quality, not framework migration. Introducing a build pipeline on a 3-week timeline is a distraction.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. What Changes From Current Design
|
|
||||||
|
|
||||||
| Area | Current | New |
|
|
||||||
|---|---|---|
|
|
||||||
| Typography | System sans-serif only | DM Serif Display for headings + DM Sans for UI |
|
|
||||||
| Accent color | `#0066cc` (generic blue) | `#1F6B5A` (deep teal) |
|
|
||||||
| Background | `#ffffff` (pure white) | `#F7F5F2` (warm paper) |
|
|
||||||
| Entry cards | Thumbnail + text below | Full-bleed 16:9 photo with overlay |
|
|
||||||
| Header | No visual identity | Accent top-border, typographic title |
|
|
||||||
| Design tokens | Hardcoded values throughout | CSS custom properties throughout |
|
|
||||||
| Post form | Lat/lng visible inputs | Lat/lng hidden, single status line |
|
|
||||||
| Font loading | None | Google Fonts DM pairing |
|
|
||||||
| Hover states | Minimal | Photo zoom, title color change |
|
|
||||||
| Stat numbers | `#0066cc` | `--color-accent` (#1F6B5A) |
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
# Milestone 1 Spec — Entry Enrichment
|
|
||||||
|
|
||||||
**Goal:** Every entry is richer out of the box — location name shown, weather auto-captured, photos in a proper gallery, hero image visible on the feed.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## User Stories
|
|
||||||
|
|
||||||
- As a traveler (Mischa), when I submit the post form, I want my current weather conditions auto-filled so I don't have to look them up manually.
|
|
||||||
- As a traveler, I want to type my city and country once and have it appear on the entry and in the feed card, so readers know where I am without reading the whole post.
|
|
||||||
- As a reader, when I scan the feed, I want to see a thumbnail photo and location for each entry so I can quickly get a sense of where Mischa is and whether to read the full entry.
|
|
||||||
- As a reader, when I open an entry, I want to see all uploaded photos in a gallery I can browse, not a wall of raw images.
|
|
||||||
- As a traveler, when I submit a form without photos, the entry should still display cleanly with no broken image placeholders.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Feature Details
|
|
||||||
|
|
||||||
### 1.1 — Location Name Field on Post Form
|
|
||||||
|
|
||||||
**What:** Add two text fields to the post form: `location_city` and `location_country`.
|
|
||||||
|
|
||||||
**Behavior:**
|
|
||||||
- Both are optional (GPS coordinates are also optional)
|
|
||||||
- Placeholder text: "e.g. Kyoto" and "e.g. Japan"
|
|
||||||
- Displayed below the lat/lng fields
|
|
||||||
- On submit, stored in entry frontmatter as `location_city` and `location_country`
|
|
||||||
- On the form, shown as a single labeled group "Location Name" with two side-by-side inputs on desktop, stacked on mobile
|
|
||||||
|
|
||||||
**Edge cases:**
|
|
||||||
- If left blank: entry shows no location badge. No error, no broken UI.
|
|
||||||
- Long city names (e.g. "Ulaanbaatar") must not overflow card layout.
|
|
||||||
- Special characters (accents, non-Latin) must render correctly.
|
|
||||||
|
|
||||||
**Mobile behavior:** Both fields full-width, stacked, 44px min touch targets.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 1.2 — Weather Auto-Fetch on Post Form
|
|
||||||
|
|
||||||
**What:** A "Get Weather" button on the post form that calls the Open-Meteo free API (no API key) using the lat/lng already entered, and fills hidden weather fields.
|
|
||||||
|
|
||||||
**Fields to fetch and store:**
|
|
||||||
- `weather_temp_c` — temperature in Celsius (integer)
|
|
||||||
- `weather_desc` — short description: one of: Sunny, Partly cloudy, Cloudy, Foggy, Drizzle, Rain, Snow, Thunderstorm (derived from WMO weather code)
|
|
||||||
|
|
||||||
**WMO code mapping (Open-Meteo uses WMO codes):**
|
|
||||||
- 0 → Sunny
|
|
||||||
- 1,2 → Partly cloudy
|
|
||||||
- 3 → Cloudy
|
|
||||||
- 45,48 → Foggy
|
|
||||||
- 51,53,55,56,57 → Drizzle
|
|
||||||
- 61,63,65,66,67,80,81,82 → Rain
|
|
||||||
- 71,73,75,77,85,86 → Snow
|
|
||||||
- 95,96,99 → Thunderstorm
|
|
||||||
|
|
||||||
**API call:**
|
|
||||||
```
|
|
||||||
https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lng}¤t=temperature_2m,weather_code&temperature_unit=celsius
|
|
||||||
```
|
|
||||||
|
|
||||||
**UX flow:**
|
|
||||||
1. User fills in lat/lng (manually or via "Get Location" button)
|
|
||||||
2. User taps "Get Weather" button
|
|
||||||
3. Button shows "Fetching…" while loading
|
|
||||||
4. On success: fills temp and desc fields (visible, editable text inputs)
|
|
||||||
5. On failure (no network, no lat/lng): shows inline error "Could not fetch weather — enter manually"
|
|
||||||
|
|
||||||
**Edge cases:**
|
|
||||||
- If lat/lng not filled when button tapped: show inline error "Enter coordinates first"
|
|
||||||
- Weather fields are always editable manually (auto-fill is a convenience, not mandatory)
|
|
||||||
- If weather fields left blank: entry shows no weather badge. No broken UI.
|
|
||||||
- Open-Meteo returns current conditions, not historical — this is fine for posting in real time
|
|
||||||
|
|
||||||
**Mobile behavior:** "Get Weather" button is full-width, 44px height, placed immediately below the lat/lng + location name fields.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 1.3 — Weather Display on Entry Page
|
|
||||||
|
|
||||||
**What:** If `weather_temp_c` or `weather_desc` is present in frontmatter, display a weather badge on the entry page.
|
|
||||||
|
|
||||||
**Display format:** `☀️ Sunny · 28°C` (icon + description + temperature)
|
|
||||||
- Icon chosen from a small set based on `weather_desc`:
|
|
||||||
- Sunny → ☀️
|
|
||||||
- Partly cloudy → ⛅
|
|
||||||
- Cloudy → ☁️
|
|
||||||
- Foggy → 🌫️
|
|
||||||
- Drizzle → 🌦️
|
|
||||||
- Rain → 🌧️
|
|
||||||
- Snow → ❄️
|
|
||||||
- Thunderstorm → ⛈️
|
|
||||||
|
|
||||||
**Placement:** In the entry header, between the date and the body text. Same line as GPS coordinates if those are shown.
|
|
||||||
|
|
||||||
**Edge cases:**
|
|
||||||
- Only temp, no desc → show temp only
|
|
||||||
- Only desc, no temp → show desc only
|
|
||||||
- Neither → hide weather section entirely
|
|
||||||
- Temperature should always be integer (round if float)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 1.4 — Location Badge on Feed Cards and Entry Page
|
|
||||||
|
|
||||||
**What:** Display `location_city, location_country` as a small badge on tracker feed cards and at the top of entry pages.
|
|
||||||
|
|
||||||
**Feed card:** Below the date, above the excerpt. Format: `📍 Kyoto, Japan`
|
|
||||||
|
|
||||||
**Entry page:** In the header below the date, above the content. Format: `📍 Kyoto, Japan`
|
|
||||||
|
|
||||||
**Edge cases:**
|
|
||||||
- Only city, no country → `📍 Kyoto`
|
|
||||||
- Only country, no city → `📍 Japan`
|
|
||||||
- Neither → location badge hidden entirely
|
|
||||||
- Long location names: truncate with ellipsis at 30 chars on cards (full text on entry page)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 1.5 — Photo Gallery on Entry Page
|
|
||||||
|
|
||||||
**What:** Photos uploaded to an entry should display in a responsive grid gallery with lightbox (click to enlarge).
|
|
||||||
|
|
||||||
**Implementation approach:** Use Grav's native media collection for the entry page. Each `.entry` folder contains its photos. Render them in a grid in `entry.html.twig`. Use a minimal vanilla JS lightbox — no external framework.
|
|
||||||
|
|
||||||
**Gallery behavior:**
|
|
||||||
- Photos displayed in a 2-column grid on mobile, 3-column on desktop
|
|
||||||
- Each thumbnail is square-cropped, 150px on mobile
|
|
||||||
- Clicking/tapping a thumbnail opens a lightbox overlay
|
|
||||||
- Lightbox: dark overlay, full-size image centered, tap/click outside or press Escape to close
|
|
||||||
- Left/right navigation arrows in lightbox (swipe on mobile)
|
|
||||||
- No captions needed for v1
|
|
||||||
|
|
||||||
**Edge cases:**
|
|
||||||
- 0 photos: gallery section hidden entirely
|
|
||||||
- 1 photo: still uses grid (single item), lightbox works
|
|
||||||
- Many photos (>10): gallery still renders (no hard limit on display)
|
|
||||||
- Non-image files in the media folder: skip them (only render jpg, jpeg, png, webp, gif)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 1.6 — Hero Image on Tracker Feed Cards
|
|
||||||
|
|
||||||
**What:** If an entry has photos, the first photo (or the one named in `hero_image` frontmatter) appears as a thumbnail on the tracker feed card.
|
|
||||||
|
|
||||||
**Implementation:** In `tracker.html.twig`, for each entry:
|
|
||||||
1. If `entry.header.hero_image` is set, use `entry.media[entry.header.hero_image]`
|
|
||||||
2. Else, use the first image in `entry.media` sorted by name
|
|
||||||
3. Render as a 16:9 aspect-ratio thumbnail, full width of card, above the title
|
|
||||||
|
|
||||||
**Edge cases:**
|
|
||||||
- No photos: card shows no image, just text. No broken `<img>` tag.
|
|
||||||
- `hero_image` set but file missing: fall back to first media file, or no image
|
|
||||||
- Very tall/wide images: CSS `object-fit: cover` maintains card aspect ratio
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Out of Scope (Milestone 1)
|
|
||||||
|
|
||||||
- Map features (Milestone 2)
|
|
||||||
- Statistics page (Milestone 3)
|
|
||||||
- Video support
|
|
||||||
- Comments or reactions
|
|
||||||
- Automated reverse geocoding (city name comes from form input, not auto-detected)
|
|
||||||
- Altitude display (data may not be present)
|
|
||||||
- Historical weather (Open-Meteo current endpoint only)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
1. Post form has `location_city` and `location_country` fields that save to entry frontmatter
|
|
||||||
2. Post form has "Get Weather" button that fills `weather_temp_c` and `weather_desc` via Open-Meteo when lat/lng are provided
|
|
||||||
3. Entry page shows weather badge when weather fields are present; hidden when absent
|
|
||||||
4. Entry page shows location badge `📍 City, Country` when location fields are present; hidden when absent
|
|
||||||
5. Tracker feed card shows location badge when present
|
|
||||||
6. Tracker feed card shows a hero image when photos exist for an entry
|
|
||||||
7. Entry page shows a 2-col (mobile) / 3-col (desktop) photo grid
|
|
||||||
8. Clicking any photo opens a full-screen lightbox with prev/next navigation
|
|
||||||
9. Pressing Escape or clicking outside lightbox closes it
|
|
||||||
10. All fields are optional — empty values produce no broken UI elements
|
|
||||||
11. All interactive elements meet 44px minimum touch target on mobile
|
|
||||||
12. Form submits correctly with all new fields populated or all blank
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Design Notes
|
|
||||||
|
|
||||||
- Weather and location badges should be subtle — small text, muted color, not the visual focus
|
|
||||||
- Use emoji icons for weather — universal, no icon font dependency
|
|
||||||
- Gallery grid: `gap: 4px` between thumbs, no borders, square crops
|
|
||||||
- Lightbox: `background: rgba(0,0,0,0.92)`, image centered with `max-height: 90vh`
|
|
||||||
- Feed card image: `aspect-ratio: 16/9`, `object-fit: cover`, rounded top corners matching card
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
# Milestone 2 Spec — Interactive Map
|
|
||||||
|
|
||||||
**Goal:** A `/map` page shows all entries as markers on an interactive Leaflet.js map, connected by a chronological route line, with popups linking to entries.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## User Stories
|
|
||||||
|
|
||||||
- As a reader, I want to see a world map showing where Mischa has been so I can understand the journey at a glance without reading every entry.
|
|
||||||
- As a reader, I want to click a map marker and see the entry date, title, and a thumbnail — and be able to click through to the full entry.
|
|
||||||
- As a reader on mobile, I want to pan and pinch-zoom the map with my fingers without the page scrolling underneath.
|
|
||||||
- As a traveler (Mischa), I want the map to automatically include every entry that has lat/lng data — I should not need to do any manual map maintenance.
|
|
||||||
- As a reader, I want the map to show the route line connecting stops in the order they were visited, so the journey makes narrative sense.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Feature Details
|
|
||||||
|
|
||||||
### 2.1 — Map Page
|
|
||||||
|
|
||||||
**Route:** `/map`
|
|
||||||
|
|
||||||
**Template:** `map.html.twig` — extends `partials/base.html.twig`
|
|
||||||
|
|
||||||
**Page file:** `user/pages/03.map/map.md`
|
|
||||||
|
|
||||||
**Content:**
|
|
||||||
- Full-viewport-height map container below the site header
|
|
||||||
- Leaflet.js loaded from CDN (jsDelivr): `https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.min.js`
|
|
||||||
- Leaflet CSS from same CDN
|
|
||||||
- Tile layer: OpenStreetMap (free, no API key): `https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png`
|
|
||||||
- Attribution: "© OpenStreetMap contributors"
|
|
||||||
|
|
||||||
**Map initialization:**
|
|
||||||
- Default zoom: auto-fit to bounds of all markers (use `map.fitBounds()`)
|
|
||||||
- If no entries with GPS data: show world view, zoom 2, centered at 0,0 with a message "No locations yet"
|
|
||||||
- Min zoom: 2, Max zoom: 18
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2.2 — Entry Data Serialization
|
|
||||||
|
|
||||||
**How entries reach the map JS:**
|
|
||||||
|
|
||||||
In `map.html.twig`, Grav's Twig will iterate all published entries under `/tracker` and serialize them to a JSON array embedded in a `<script>` tag:
|
|
||||||
|
|
||||||
```js
|
|
||||||
var ENTRIES = [
|
|
||||||
{
|
|
||||||
"lat": 48.8566,
|
|
||||||
"lng": 2.3522,
|
|
||||||
"title": "Paris morning",
|
|
||||||
"date": "2026-06-18",
|
|
||||||
"url": "/tracker/2026-06-18",
|
|
||||||
"hero": "/path/to/thumb.jpg" // null if no photo
|
|
||||||
},
|
|
||||||
...
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
**Only entries with valid lat AND lng are included** (skip entries where either is empty/null).
|
|
||||||
|
|
||||||
Entries sorted ascending by date (oldest first) so the route line is drawn in travel order.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2.3 — Route Polyline
|
|
||||||
|
|
||||||
**What:** A colored line drawn between entry markers in chronological order.
|
|
||||||
|
|
||||||
**Style:**
|
|
||||||
- Color: `#0066cc` (brand blue, matches existing CSS)
|
|
||||||
- Weight: 3px
|
|
||||||
- Opacity: 0.7
|
|
||||||
- No arrow heads for v1
|
|
||||||
|
|
||||||
**Behavior:**
|
|
||||||
- Line drawn between consecutive entries (by date) that have valid GPS
|
|
||||||
- If only 1 entry: no line (just a single marker)
|
|
||||||
- If two consecutive entries are very far apart (>5000km): line still drawn — it's a flight, expected
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2.4 — Entry Markers
|
|
||||||
|
|
||||||
**What:** One circular marker per entry with GPS coordinates.
|
|
||||||
|
|
||||||
**Marker design:**
|
|
||||||
- Custom circular marker (not default Leaflet teardrop)
|
|
||||||
- Color: `#0066cc` fill, white border, 2px border
|
|
||||||
- Size: 12px diameter on mobile, 14px on desktop
|
|
||||||
- Most recent entry: larger (18px) and brighter color to indicate "current location"
|
|
||||||
|
|
||||||
**Popup on click/tap:**
|
|
||||||
```
|
|
||||||
[thumbnail if available — 120px wide, 80px tall, cover cropped]
|
|
||||||
📅 18 June 2026
|
|
||||||
Paris morning
|
|
||||||
[Read entry →]
|
|
||||||
```
|
|
||||||
- Popup width: 180px max
|
|
||||||
- "Read entry →" links to the entry page
|
|
||||||
- Tapping outside popup closes it
|
|
||||||
|
|
||||||
**Edge cases:**
|
|
||||||
- Two entries at the same lat/lng: Leaflet clusters or offsets them slightly (use small offset to prevent exact overlap — just add 0.0001° offset per duplicate)
|
|
||||||
- Entry with GPS but no photo: popup shows no image, just date + title + link
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2.5 — Mobile Map UX
|
|
||||||
|
|
||||||
**Problem:** On mobile, a map inside a scrollable page creates a scroll-trap (finger intended for page scroll gets captured by map pan).
|
|
||||||
|
|
||||||
**Solution:**
|
|
||||||
- Map container is `height: calc(100vh - 60px)` (full viewport minus header)
|
|
||||||
- Map is the primary content of the page — no scroll needed
|
|
||||||
- `touch-action: none` on the map container prevents page scroll interference
|
|
||||||
- Leaflet handles touch pan/zoom natively
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 2.6 — Navigation Link
|
|
||||||
|
|
||||||
**What:** "Map" link added to the site header navigation.
|
|
||||||
|
|
||||||
**Where:** `partials/base.html.twig` nav section — add `<a href="{{ base_url_absolute }}/map">Map</a>`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Out of Scope (Milestone 2)
|
|
||||||
|
|
||||||
- Filtering markers by date range
|
|
||||||
- Clustering markers at low zoom levels
|
|
||||||
- Heatmap or density visualization
|
|
||||||
- Showing the route on the tracker feed page (Milestone 4)
|
|
||||||
- Showing elevation profile
|
|
||||||
- Country highlight/fill on the map
|
|
||||||
- Offline map tiles
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
1. `/map` page exists and returns HTTP 200
|
|
||||||
2. Page renders a full-height interactive map
|
|
||||||
3. All published entries with valid lat/lng appear as markers
|
|
||||||
4. Markers are connected by a route line in date order
|
|
||||||
5. Clicking/tapping a marker shows a popup with date, title, and link
|
|
||||||
6. Popup link navigates to the correct entry page
|
|
||||||
7. Most recent entry marker is visually distinct (larger/brighter)
|
|
||||||
8. If no entries have GPS: map renders at world zoom with "No locations yet" message
|
|
||||||
9. Map is pannable and zoomable by touch on mobile
|
|
||||||
10. "Map" link appears in site navigation and routes to `/map`
|
|
||||||
11. Map auto-fits to show all markers on page load
|
|
||||||
12. Entries without lat/lng are silently excluded (no JS errors)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Design Notes
|
|
||||||
|
|
||||||
- Map tile layer: OpenStreetMap default tiles. Clean, recognizable, free.
|
|
||||||
- Keep the Grav site header visible above the map — don't go full-screen (users need the nav)
|
|
||||||
- Popup design: minimal. White background, slight box-shadow, 8px border-radius
|
|
||||||
- Do not use any Leaflet plugins beyond the core library — keep the dependency footprint tiny
|
|
||||||
- The map page should load fast: Leaflet is ~42KB gzipped. Tile images load progressively. No blocking.
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
# Milestone 3 Spec — Statistics Page
|
|
||||||
|
|
||||||
**Goal:** A `/stats` page showing key trip numbers: days on the road, entries posted, countries visited, and approximate distance traveled.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## User Stories
|
|
||||||
|
|
||||||
- As a reader, I want to see a quick summary of how far Mischa has traveled and how many countries they've visited, without having to read every entry.
|
|
||||||
- As a traveler (Mischa), I want to see my own trip stats at a glance — a satisfying progress indicator while traveling.
|
|
||||||
- As a reader, I want stats that update automatically as new entries are posted — no manual maintenance.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Feature Details
|
|
||||||
|
|
||||||
### 3.1 — Stats Page
|
|
||||||
|
|
||||||
**Route:** `/stats`
|
|
||||||
|
|
||||||
**Template:** `stats.html.twig` — extends `partials/base.html.twig`
|
|
||||||
|
|
||||||
**Page file:** `user/pages/04.stats/stats.md`
|
|
||||||
|
|
||||||
**Computed in Twig** (server-side, from published entries under `/tracker`):
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3.2 — Stat: Days on the Road
|
|
||||||
|
|
||||||
**Definition:** Number of calendar days from the date of the first published entry to today.
|
|
||||||
|
|
||||||
**Formula (Twig):**
|
|
||||||
```twig
|
|
||||||
{% set first_entry = entries|first %}
|
|
||||||
{% set days = (now.timestamp - first_entry.date|date('U'))|round / 86400 %}
|
|
||||||
{% set days_on_road = [days|round(0, 'floor'), 0]|max %}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Display:** `42 days on the road`
|
|
||||||
|
|
||||||
**Edge cases:**
|
|
||||||
- No entries: show `0 days on the road` or `Trip not started yet`
|
|
||||||
- Only one entry (today): show `1 day on the road`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3.3 — Stat: Entries Posted
|
|
||||||
|
|
||||||
**Definition:** Count of all published entries under `/tracker`.
|
|
||||||
|
|
||||||
**Display:** `17 entries posted`
|
|
||||||
|
|
||||||
**Edge cases:**
|
|
||||||
- 0 entries: `0 entries posted`
|
|
||||||
- 1 entry: `1 entry posted` (singular)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3.4 — Stat: Countries Visited
|
|
||||||
|
|
||||||
**Definition:** Unique values of `location_country` across all published entries, non-empty.
|
|
||||||
|
|
||||||
**Display:** Count + list
|
|
||||||
|
|
||||||
```
|
|
||||||
6 countries visited
|
|
||||||
Japan · South Korea · Mongolia · Russia · Finland · Estonia
|
|
||||||
```
|
|
||||||
|
|
||||||
**Edge cases:**
|
|
||||||
- No entries have `location_country`: show `Countries: —`
|
|
||||||
- Some entries missing `location_country`: count only those that have it; note "(based on X of Y entries)"
|
|
||||||
- Duplicate country names are de-duplicated (case-insensitive)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3.5 — Stat: Approximate Distance Traveled
|
|
||||||
|
|
||||||
**Definition:** Sum of great-circle (haversine) distances between consecutive entries that have valid lat/lng, in ascending date order.
|
|
||||||
|
|
||||||
**Implementation:** Computed in Twig using a haversine formula macro.
|
|
||||||
|
|
||||||
**Haversine in Twig:**
|
|
||||||
```twig
|
|
||||||
{% macro haversine(lat1, lng1, lat2, lng2) %}
|
|
||||||
{% set R = 6371 %}
|
|
||||||
{% set dLat = ((lat2 - lat1) * 3.14159265 / 180) %}
|
|
||||||
{% set dLng = ((lng2 - lng1) * 3.14159265 / 180) %}
|
|
||||||
{% set a = (dLat/2)|sin * (dLat/2)|sin + (lat1 * 3.14159265 / 180)|cos * (lat2 * 3.14159265 / 180)|cos * (dLng/2)|sin * (dLng/2)|sin %}
|
|
||||||
{% set c = 2 * a|sqrt|asin %}
|
|
||||||
{{ (R * c)|round }}
|
|
||||||
{% endmacro %}
|
|
||||||
```
|
|
||||||
|
|
||||||
Note: Twig does not have `sin`/`cos`/`asin`/`sqrt` built-in. Use a JavaScript-side calculation instead:
|
|
||||||
|
|
||||||
**Implementation:** Embed the entry GPS data as JSON in the template (same pattern as Milestone 2), compute distance in vanilla JS, and write the result into the DOM on page load.
|
|
||||||
|
|
||||||
```js
|
|
||||||
function haversine(lat1, lng1, lat2, lng2) {
|
|
||||||
var R = 6371;
|
|
||||||
var dLat = (lat2 - lat1) * Math.PI / 180;
|
|
||||||
var dLng = (lng2 - lng1) * Math.PI / 180;
|
|
||||||
var a = Math.sin(dLat/2)**2 + Math.cos(lat1*Math.PI/180) * Math.cos(lat2*Math.PI/180) * Math.sin(dLng/2)**2;
|
|
||||||
return R * 2 * Math.asin(Math.sqrt(a));
|
|
||||||
}
|
|
||||||
var total = 0;
|
|
||||||
for (var i = 1; i < GPS_POINTS.length; i++) {
|
|
||||||
total += haversine(GPS_POINTS[i-1][0], GPS_POINTS[i-1][1], GPS_POINTS[i][0], GPS_POINTS[i][1]);
|
|
||||||
}
|
|
||||||
document.getElementById('stat-distance').textContent = Math.round(total).toLocaleString() + ' km';
|
|
||||||
```
|
|
||||||
|
|
||||||
**Display:** `~3,400 km traveled`
|
|
||||||
|
|
||||||
**Edge cases:**
|
|
||||||
- 0 or 1 GPS points: `Distance: —`
|
|
||||||
- Very large numbers (trans-continental trip): use thousands separator: `12,400 km`
|
|
||||||
- Disclaimer note: "approximate — based on straight lines between entry locations"
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3.6 — Visual Layout
|
|
||||||
|
|
||||||
**Layout:** 4 large stat blocks in a 2×2 grid on desktop, stacked on mobile.
|
|
||||||
|
|
||||||
Each block:
|
|
||||||
```
|
|
||||||
┌─────────────────┐
|
|
||||||
│ 42 │
|
|
||||||
│ days on road │
|
|
||||||
└─────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
- Number: large (3rem), bold, brand blue
|
|
||||||
- Label: small (0.85rem), muted grey
|
|
||||||
- Background: white, 1px border, 8px radius, subtle shadow
|
|
||||||
- Mobile: 2-col grid (2 stats per row)
|
|
||||||
|
|
||||||
Below the grid: list of countries visited (plain text, centered, muted).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 3.7 — Navigation Link
|
|
||||||
|
|
||||||
Add "Stats" to the site navigation in `partials/base.html.twig`.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Out of Scope (Milestone 3)
|
|
||||||
|
|
||||||
- Charts or graphs (bar charts, line graphs, etc.)
|
|
||||||
- World map with highlighted countries (that's a visual enhancement, deferred)
|
|
||||||
- Per-country breakdown (km in each country, days in each country)
|
|
||||||
- Speed statistics (km/day average)
|
|
||||||
- Elevation statistics
|
|
||||||
- Historical comparison (vs. last trip)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
1. `/stats` page exists and returns HTTP 200
|
|
||||||
2. "Days on the road" shows correct count from first entry date to today
|
|
||||||
3. "Entries posted" shows count of published entries
|
|
||||||
4. "Countries visited" shows correct count + list of unique non-empty `location_country` values
|
|
||||||
5. "Distance traveled" shows km sum of haversine distances between consecutive GPS entries
|
|
||||||
6. All four stats display in a 2×2 grid on desktop
|
|
||||||
7. On mobile (375px), stats stack into a 2-column responsive grid
|
|
||||||
8. Stats auto-update when new entries are published (no manual maintenance)
|
|
||||||
9. If no entries: all stats show 0 or `—`, no JS errors
|
|
||||||
10. "Stats" link in navigation routes to `/stats`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Design Notes
|
|
||||||
|
|
||||||
- Stats should feel like a dashboard, not a table — big numbers, small labels
|
|
||||||
- Do not use any external charting library for v1
|
|
||||||
- Countries list below the grid: inline, separated by `·`, muted grey
|
|
||||||
- The "approximate" disclaimer for distance should be in small print below the distance stat
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
# Milestone 4 Spec — Mini-Map on Tracker Feed
|
|
||||||
|
|
||||||
**Goal:** Embed a compact interactive map above the entry feed on the tracker page, showing recent entry positions and the current location, giving readers immediate spatial context.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## User Stories
|
|
||||||
|
|
||||||
- As a reader landing on the tracker feed, I want to immediately see where Mischa currently is without having to navigate to the full map page.
|
|
||||||
- As a reader, I want to click a marker on the mini-map and jump to that entry.
|
|
||||||
- As a traveler (Mischa), I want the feed page to feel like a live travel dashboard, not just a blog list.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Feature Details
|
|
||||||
|
|
||||||
### 4.1 — Mini-Map Placement
|
|
||||||
|
|
||||||
**Where:** At the top of `tracker.html.twig`, before the entry card list.
|
|
||||||
|
|
||||||
**Height:** 240px on mobile, 320px on desktop.
|
|
||||||
|
|
||||||
**Width:** Full width of content column (max 680px).
|
|
||||||
|
|
||||||
**Tile layer:** Same OpenStreetMap tiles as Milestone 2.
|
|
||||||
|
|
||||||
**No duplicate Leaflet load:** Leaflet is already loaded on the map page; on the tracker page, load it only if needed. Check with `if (typeof L === 'undefined')` before initializing. (In practice, the CSS and JS are loaded unconditionally from the same CDN — caching handles it.)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4.2 — What's Shown
|
|
||||||
|
|
||||||
- **All entries with GPS** shown as small markers (not just recent 10 — the map auto-fits to bounds)
|
|
||||||
- **Route line** connecting them in chronological order (same style as Milestone 2)
|
|
||||||
- **Most recent marker** highlighted (larger, brighter)
|
|
||||||
- **No popups by default** — tapping a marker links directly to the entry (no popup intermediary for the mini-map, keeps it fast)
|
|
||||||
- Map auto-fits bounds to all markers; if only 1 marker, zoom to 10
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4.3 — Interaction
|
|
||||||
|
|
||||||
- Tap/click marker → navigate to entry URL directly
|
|
||||||
- Map is pannable and zoomable (same touch handling as M2)
|
|
||||||
- "View full map →" link below the mini-map → navigates to `/map`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4.4 — Entry Data
|
|
||||||
|
|
||||||
Same JSON serialization as Milestone 2 (embed `TRACKER_ENTRIES` in the Twig template). This can reuse the same data variable name if both map and tracker pages use the same template pattern.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 4.5 — Empty State
|
|
||||||
|
|
||||||
If no entries have GPS coordinates:
|
|
||||||
- Mini-map hidden entirely (don't show an empty world map on the feed page)
|
|
||||||
- Entry list still shows normally
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Out of Scope (Milestone 4)
|
|
||||||
|
|
||||||
- Clustering markers at low zoom
|
|
||||||
- Filtering by date
|
|
||||||
- Satellite/terrain tile layers
|
|
||||||
- Search on the mini-map
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Acceptance Criteria
|
|
||||||
|
|
||||||
1. Mini-map appears above entry cards on the tracker feed page
|
|
||||||
2. All entries with valid lat/lng appear as markers on the mini-map
|
|
||||||
3. Route line connects markers in date order
|
|
||||||
4. Most recent marker is visually distinct
|
|
||||||
5. Clicking/tapping a marker navigates directly to that entry
|
|
||||||
6. "View full map →" link appears below the mini-map and routes to `/map`
|
|
||||||
7. If no entries have GPS, mini-map is hidden and entry list shows normally
|
|
||||||
8. Mini-map is pannable and zoomable by touch on mobile
|
|
||||||
9. Mini-map does not block page scrolling on mobile (map is fixed height, not full-screen)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Design Notes
|
|
||||||
|
|
||||||
- Mini-map border-radius should match the card design (8px)
|
|
||||||
- Light 1px border or subtle shadow to separate from content
|
|
||||||
- "View full map →" in small muted text, right-aligned
|
|
||||||
- Keep the mini-map lightweight: same Leaflet instance, no additional plugins
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
# PM Analysis — What to Build (and What to Skip)
|
|
||||||
|
|
||||||
*Role: Senior Product Manager. Audience: one solo traveler (Mischa), platform: Grav CMS flat-file PHP, no native app.*
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Starting position
|
|
||||||
|
|
||||||
Polarsteps and FindPenguins are native mobile apps built around:
|
|
||||||
1. Background GPS tracking (requires OS-level access)
|
|
||||||
2. Social networks (followers, discovery, comments)
|
|
||||||
3. App-side video/reel processing
|
|
||||||
|
|
||||||
**None of these three pillars are reproducible in a web CMS.** Any plan that tries to replicate them wholesale is delusional. What we can do is cherry-pick the *outputs* — the things those apps display to readers — and build them into the blog in ways that add real value to both Mischa (the poster) and readers (friends/family following along).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Feature-by-Feature Audit
|
|
||||||
|
|
||||||
| Feature | Makes sense solo? | Buildable in Grav+JS? | Value to readers? | Worth the cost? | Decision |
|
|
||||||
|---|---|---|---|---|---|
|
|
||||||
| Auto background GPS tracking | No — posting manually anyway | No — requires native app | — | — | **SKIP** |
|
|
||||||
| Interactive map of visited locations | Yes | Yes — Leaflet.js + frontmatter lat/lng | High | High | **BUILD** |
|
|
||||||
| Route line on map between entries | Yes | Yes — connect entry coords in order | High | Medium | **BUILD** |
|
|
||||||
| Entry location name (city, country) | Yes | Yes — manual input on form | High | Low | **BUILD** |
|
|
||||||
| Weather metadata per entry | Yes | Yes — Open-Meteo free API, no key needed | Medium | Medium | **BUILD** |
|
|
||||||
| Photo gallery per entry | Yes | Yes — shortcode-gallery-plusplus installed | High | Low | **BUILD** (already partial) |
|
|
||||||
| Hero image on feed cards | Yes | Yes — already in frontmatter | High | Low | **BUILD** |
|
|
||||||
| Trip statistics page | Yes | Yes — compute from frontmatter | Medium | Low | **BUILD** |
|
|
||||||
| Countries visited world map | Yes | Yes — highlight SVG or Leaflet layers | Medium | Medium | **BUILD** |
|
|
||||||
| Follower system | No — solo blog | Would need auth + DB | None | — | **SKIP** |
|
|
||||||
| Comments on entries | No — spam risk, no community | Would need plugin + moderation | Minimal | — | **SKIP** |
|
|
||||||
| Social discovery / explore | No — not a platform | Would need indexing infrastructure | None | — | **SKIP** |
|
|
||||||
| Group trip / travel buddies | No — solo trip | — | — | — | **SKIP** |
|
|
||||||
| Reactions / likes | No | — | — | — | **SKIP** |
|
|
||||||
| 3D flyover video | No — proprietary pipeline | No | Nice | — | **SKIP** |
|
|
||||||
| Trip reels / short video | No — app-side processing | No | Nice | — | **SKIP** |
|
|
||||||
| Travel book / print | No — out of scope | No | — | — | **SKIP** |
|
|
||||||
| AI itinerary builder | No — trip already started | No | — | — | **SKIP** |
|
|
||||||
| Flight detection | No — requires native app sensors | No | — | — | **SKIP** |
|
|
||||||
| Delayed sharing / live location | No — blog posts after the fact | Irrelevant | — | — | **SKIP** |
|
|
||||||
| Offline posting | Already works | Already works (Grav form offline) | — | — | **ALREADY EXISTS** |
|
|
||||||
| Scheduled / draft posts | Already exists | Already exists (publish_date) | — | — | **ALREADY EXISTS** |
|
|
||||||
| Step suggestions / nudges | No — push notifications not possible | No | — | — | **SKIP** |
|
|
||||||
| Eebook / export | No — out of scope | Possible but niche | — | — | **SKIP** |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What to Build — Summary
|
|
||||||
|
|
||||||
### Keep (already exists, just needs to work reliably)
|
|
||||||
- Login-gated mobile posting form ✓
|
|
||||||
- Draft and scheduled publishing ✓
|
|
||||||
|
|
||||||
### Build
|
|
||||||
|
|
||||||
**1. Entry enrichment** — make each entry richer with zero extra effort from Mischa:
|
|
||||||
- Location name (city, country) captured at post time
|
|
||||||
- Weather auto-fetched via Open-Meteo at post time using lat/lng
|
|
||||||
- Photos displayed in a proper gallery (lightbox)
|
|
||||||
- Hero image shown on feed card
|
|
||||||
|
|
||||||
**2. Interactive map** — the single most "Polarsteps-like" thing that's genuinely achievable:
|
|
||||||
- `/map` page with Leaflet.js
|
|
||||||
- Marker per entry (lat/lng from frontmatter)
|
|
||||||
- Route line connecting entries in date order
|
|
||||||
- Popup with title, date, thumbnail, link to entry
|
|
||||||
- Mobile-friendly (touch pan/zoom)
|
|
||||||
|
|
||||||
**3. Trip statistics** — a simple stats page:
|
|
||||||
- Days on the road (count of entries with distinct dates)
|
|
||||||
- Entries posted
|
|
||||||
- Countries/regions visited (derived from location name field)
|
|
||||||
- Approx distance traveled (sum of haversine distances between GPS points)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## What to Skip — with reasons
|
|
||||||
|
|
||||||
| Feature | Reason skipped |
|
|
||||||
|---|---|
|
|
||||||
| Background GPS tracking | Requires native app. Grav runs on a server. |
|
|
||||||
| Social features (followers, comments, likes) | Adds spam risk, moderation burden, zero value for a solo travel blog with a personal audience. A "share link" is enough. |
|
|
||||||
| Video reels | App-side video processing pipeline, not available in a web CMS. |
|
|
||||||
| 3D flyover | Proprietary rendering. Not worth building from scratch. |
|
|
||||||
| Travel book printing | Out of scope. Mischa can use Polarsteps or FindPenguins for this if desired. |
|
|
||||||
| AI itinerary builder | Trip is already in progress. Out of scope. |
|
|
||||||
| Discovery / explore | Not a platform. No community. |
|
|
||||||
| Group trips | Solo traveler. |
|
|
||||||
| Flight detection | Requires native OS sensor access. |
|
|
||||||
| Delayed sharing | Moot — we don't broadcast real-time location at all. |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Milestone Plan
|
|
||||||
|
|
||||||
### Milestone 1 — Entry Enrichment (2–3 days)
|
|
||||||
**Goal:** Every entry is richer out of the box — photo gallery works, location name shown, weather captured, hero image on feed.
|
|
||||||
|
|
||||||
Features:
|
|
||||||
- Location name field (city + country) added to post form and displayed on entries/cards
|
|
||||||
- Weather auto-fetch on post form (JS call to Open-Meteo using entered lat/lng, fills hidden fields)
|
|
||||||
- Weather displayed on entry page
|
|
||||||
- Photo gallery working (shortcode-gallery-plusplus or native media display)
|
|
||||||
- Hero image shown on tracker feed cards
|
|
||||||
|
|
||||||
**Value:** Immediate. Makes each entry feel like a real travel log entry, not just a text post.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Milestone 2 — Interactive Map (2–3 days)
|
|
||||||
**Goal:** A `/map` page shows all entries as markers on an interactive Leaflet.js map, connected by a route line, with popups.
|
|
||||||
|
|
||||||
Features:
|
|
||||||
- New `map` page and template
|
|
||||||
- Leaflet.js loaded from CDN (no build step)
|
|
||||||
- Entries serialized to JSON in the template (lat/lng, title, date, url, hero_image)
|
|
||||||
- Route polyline in chronological order
|
|
||||||
- Marker popup: date, title, thumbnail, "Read entry →" link
|
|
||||||
- Map added to site navigation
|
|
||||||
|
|
||||||
**Value:** High for readers — gives a bird's-eye view of the trip. The single most compelling "where is Mischa?" feature.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Milestone 3 — Statistics Page (1–2 days)
|
|
||||||
**Goal:** A `/stats` page with key trip numbers.
|
|
||||||
|
|
||||||
Features:
|
|
||||||
- Days on the road (first entry date to today)
|
|
||||||
- Total entries posted
|
|
||||||
- Unique countries visited (derived from location names)
|
|
||||||
- Approximate distance traveled (haversine between consecutive entry GPS points)
|
|
||||||
- Simple, scannable layout — no charts needed for v1
|
|
||||||
|
|
||||||
**Value:** Medium — nice context for readers, satisfying for Mischa to see progress.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Milestone 4 — Map on Tracker Feed (1 day)
|
|
||||||
**Goal:** A mini-map showing recent positions above or alongside the feed, so the first thing readers see is "where is Mischa now?"
|
|
||||||
|
|
||||||
Features:
|
|
||||||
- Small embedded Leaflet map on the tracker/feed page
|
|
||||||
- Shows last 10 entries as markers, with the most recent highlighted
|
|
||||||
- Route line between them
|
|
||||||
- Tapping a marker opens the entry
|
|
||||||
|
|
||||||
**Value:** Medium — gives context to the feed without navigating away. Nice "current location" feel.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Milestone Priority Order
|
|
||||||
|
|
||||||
**M1 first** — entry quality affects every post Mischa makes from day 1 of the trip. Get this right immediately.
|
|
||||||
|
|
||||||
**M2 second** — the map is the headline feature that makes this feel like a Polarsteps-style blog. Technically independent from M1 (uses lat/lng already in frontmatter).
|
|
||||||
|
|
||||||
**M3 third** — stats are a nice-to-have. Easy to add once M1 and M2 are stable.
|
|
||||||
|
|
||||||
**M4 fourth** — the mini-map on the feed is polish. Only worth doing once the full map (M2) is solid.
|
|
||||||