Compare commits

..

64 Commits

Author SHA1 Message Date
m038 21c1d22859 fix: PhotoSwipe bg doesn't cover bottom — add !important and pin bg to viewport 2026-06-22 09:11:52 +02:00
m038 68b328dabc feat: enrich Slovenia 2024 Piran entry with coords and weather 2026-06-22 09:07:17 +02:00
m038 817bd17959 feat: split Slovenia 2024 into its own trip
Move Piran entry out of us-canada-mex-2024 into a new slovenia-2024 trip.
Rename entry folder to match the post title convention.
Fix us-canada-mex-2024 date_start to 2024-07-21 (first actual US entry).
2026-06-22 09:06:00 +02:00
m038 77dd99ee2b feat(stories): add mini-map via shared partial, add story card IDs 2026-06-22 01:39:29 +02:00
m038 857f33be54 refactor(dailies): use shared feed-map partial 2026-06-22 01:37:33 +02:00
m038 320a98893a feat: add shared feed-map partial (dailies + stories) 2026-06-22 01:33:26 +02:00
m038 e07fb3a72a feat(map): exit fullscreen on marker click, then scroll to entry
When fullscreen is active, clicking a marker now triggers fsBtn.click()
to exit cleanly (handles class, body overflow, tripMap.resize + icon),
then waits 450ms for the exit animation before scrolling to the entry
and firing the highlight. Also fixes missing icon-swap CSS for
.home-map-col.is-fullscreen (was only targeting .feed-map-wrap).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-22 01:12:52 +02:00
m038 1bb588d1d2 fix(trip): switch panel animation to max-height (grid-template-rows broken)
grid-template-rows: 0fr fails to fully collapse when the direct grid
child has overflow:hidden (creates a BFC that prevents 0-height).
max-height: 0 → 600px with overflow:hidden is simpler and reliable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-22 01:09:38 +02:00
m038 aa1cb7411c fix(map): collapse attribution on load; darken fullscreen button
Attribution: MapLibre v4 uses <details> and may open it after load
regardless of compact:true — remove the open attribute in the load
handler to guarantee collapsed state.

Button: switch from teal to --color-canvas (#22201B) so it sits quietly
against the dark map; icon reads in --color-ink (warm cream).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-22 01:05:12 +02:00
m038 5fe8c015f1 fix(map): theme fullscreen button with accent colour
Replace plain white with --color-accent/--color-accent-on so the button
reads as a site control rather than a stray MapLibre element.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-22 01:00:50 +02:00
m038 6f9538053c feat(map): mobile fullscreen button on trip page map
Button in bottom-right of #trip-map (z-index:1000), hidden ≥769px.
Attribution moved to bottom-left to free the corner. Clicking toggles
.is-fullscreen on .home-map-col (position:fixed, 100dvh), locks body
scroll, and calls tripMap.resize() for MapLibre to re-render.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-22 00:56:57 +02:00
m038 5e503cf3a5 fix(map): fullscreen btn inside map div, attribution moved to bottom-left
Button is back inside #feed-map with z-index:1000 to clear all MapLibre
layers. Attribution control disabled in constructor and re-added to
bottom-left so bottom-right is free for the fullscreen button.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-22 00:20:23 +02:00
m038 ce860cfef9 fix(map): move fullscreen button outside feed-map div, top-right corner
MapLibre's attribution button occupies bottom-right of the container.
Moving our button out of the map div avoids MapLibre's DOM entirely,
and top-right is clear of all default MapLibre controls.
Position anchor moves to feed-map-wrap (position:relative).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 23:54:13 +02:00
m038 989755d33c feat(map): mobile fullscreen button for feed mini-map
Button in the bottom-right corner of the map, hidden ≥769px. Clicking
it toggles .is-fullscreen on .feed-map-wrap (position:fixed, full
viewport), locks body scroll, and calls feedMap.resize() so MapLibre
re-renders at the new size. Icon swaps between expand SVG and ✕.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 23:44:09 +02:00
m038 9ddf52c635 fix(trip): raise stat-value clamp floor to 2rem (32px)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 23:35:32 +02:00
m038 9b62f79301 fix(trip): raise stat-value clamp floor to --text-xl (1.75rem)
22px floor was too close to the preferred at 375px (6vw=22.5px), so
values were pinned near the minimum. 28px floor makes values pop more
on small screens while long values like ~12,366 still wrap gracefully.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 23:33:06 +02:00
m038 64aa9ec023 revert(trip): restore stat-label to --text-xs
Small label is intentional — the contrast with the larger value is the
visual hierarchy. Revert the sm bump from the previous commit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 23:30:24 +02:00
m038 9bfd96af2c fix(trip): bump stat-label to --text-sm, widen stat-value fluid range
Label: xs (12px) → sm (14px) for clearer hierarchy below the value.
Value preferred: 5.5vw → 6vw so short values stay bold on mid phones.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 23:28:42 +02:00
m038 000af6934f fix(trip): raise stat-value clamp floor to --text-lg for visual hierarchy
14px floor was too close to the 12px label size. 1.375rem keeps the
value visually dominant over the label even at minimum size.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 23:23:39 +02:00
m038 c94e36a861 fix(trip): fluid stat-value font size with clamp()
Replaces fixed 3rem with clamp(--text-sm, 5.5vw, --text-3xl) so long
values like "4:32:15" scale down on mobile instead of overflowing.
Desktop (≥870px viewport) is unchanged at 3rem.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 23:21:58 +02:00
m038 2c831628b2 fix(trip): fix cycling stats mobile grid — span lone last card full width
7 cycling stat blocks in a 2-col mobile grid leaves a lone card in the
last row's left column with empty space on the right. Using
:last-child:nth-child(odd) + grid-column: 1/-1 spans that card across
both columns. Also minmax(0,1fr) on both grids for strictly equal widths.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 23:18:09 +02:00
m038 02fc666661 feat(trip): pill radius on panel toggles, slide animation, mobile close button
- Radius: trip-panel-toggle now uses --radius-full, consistent with filter pills
- Animation: stats/cycling blocks use CSS grid-template-rows 0fr→1fr transition
  (inner trip-panel-inner div carries decoration so border/padding don't peek
  out when collapsed; display:none removed)
- Close button: ↑ Close stats / ↑ Close cycling at bottom of each panel,
  hidden ≥769px, triggers the header toggle via data-toggle attribute

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 23:09:52 +02:00
m038 c3cb224402 style(trip): give Stats/Cycling panel toggles a square bordered style
Border + 4px radius instead of borderless text, matching the visual
weight of the filter pills without the full pill roundness.
Active state gets teal border + accent-light background like other active controls.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 23:03:56 +02:00
m038 2b8ea1963b refactor(trip): declutter filter bar — move Stats/Cycling to panel toggles
Filter bar now has one job: content type (All/Journal/Stories) + sort icon (↑/↓).
Stats and Cycling move to a lean text-button row below the bar with a
rotating ▾ caret — CSS handles expand/collapse state via .is-active,
no JS changes needed for the caret animation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 22:58:28 +02:00
m038 f94880e758 feat: add sort toggle to dailies and stories pages
dailies: reverse Twig output to ascending (matching trip default),
add feed-sort-bar above feed, add sort JS using [data-type] + appendChild.

stories: wrap heading in flex header row with sort button inline,
add sort JS targeting .story-card children of .stories-grid.

CSS: feed-sort-bar (right-aligned button above feed),
stories-listing__header (flex row, baseline-aligned), heading margin moved to header.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 22:27:11 +02:00
m038 b6142cee44 feat(trip): add ascending/descending sort toggle button
Button sits in the right filter group alongside Stats/Cycling.
Default state: ascending (↑ Oldest first, no highlight).
Toggled state: descending (↓ Newest first, is-active pill style).
DOM reversal uses insertBefore against the anchored #feed-filter-empty
so the empty-state message stays last regardless of sort direction.
Interacts safely with the type filter (show/hide by data-type).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 22:21:55 +02:00
m038 53bfe5955d fix(photoswipe): target currSlide.container not currSlide.el
pswp.currSlide is a Slide instance whose DOM element is stored as
.container (.pswp__zoom-wrap). The .el property belongs to the
itemHolder wrapper, not the Slide — so currSlide.el was always
undefined, the null-guard exited early, and no animation played.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 21:45:22 +02:00
m038 9f503c011d fix(photoswipe): keyboard arrow animation via CSS keyframes
Previous approach (CSS transition + reflow trick) is unreliable in
Firefox. New approach: PhotoSwipe emits 'change' synchronously before
painting; we add a direction-aware CSS keyframe animation to the
incoming slide element, with animation-fill-mode:both so there is no
flash before the animation starts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 21:36:48 +02:00
m038 415d95ed47 feat(photoswipe): animate keyboard arrow navigation in lightbox
PhotoSwipe's goTo() moves slides instantly (no spring animation unlike
swipe). Intercepts keydown in capture phase, sets a CSS transition and
forces a reflow before PhotoSwipe moves the container, so the browser
animates from the old position to the new one. Cleans up on close.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 21:29:06 +02:00
m038 e787544a2b feat(strip): smooth scroll animation on arrow button clicks
scroll-behavior: smooth on the strip element ensures programmatic
scrollBy calls animate consistently, cooperating with scroll-snap.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 21:22:42 +02:00
m038 9f94164c61 fix(arrows): insert strip-controls after wrap, not inside it
Dots moved inside journal-photo-wrap (overflow:hidden) earlier, so
controls were being clipped. Now inserts after the wrap element.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 21:13:43 +02:00
m038 608ccfdecd fix(partial): restore data-slides on photo strip
Missing data-slides caused base.html.twig arrow script to read
slideCount as 1 and bail before creating prev/next controls.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 21:10:48 +02:00
m038 933652fd57 refactor(templates): extract entry markup into shared partials
Creates partials/entry-journal.html.twig and partials/entry-story.html.twig
so trip, dailies, and home all use the same up-to-date markup. Home page
gains PhotoSwipe, blurred fill, adaptive aspect ratio, and hash-based
marker scroll. Future changes only need to happen in one place.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 21:07:12 +02:00
m038 fdaed1033a fix(ios): use 100dvh for PhotoSwipe to fix dynamic viewport
iOS Safari freezes 100vh at the initial viewport height (address bar
visible). 100dvh tracks the live viewport as browser chrome shows/hides.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 20:48:13 +02:00
m038 bc77baca2e fix(scroll): clear URL hash when back-to-top is clicked
Uses history.pushState to strip the stale #entry-slug without
triggering a page jump, then smooth-scrolls to the top.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 20:41:41 +02:00
m038 7c9a55224a fix(scroll): use hash navigation for marker clicks
Browser handles scroll natively via window.location.hash, respecting
scroll-margin-top. Updates URL for shareability and screen reader
compatibility. Added html { scroll-behavior: smooth } for smooth
hash navigation globally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 20:40:15 +02:00
m038 85ba3747b1 fix(scroll): use block:start so scroll-margin-top is respected
block:center ignores scroll-margin-top. block:start positions the
entry's top edge at the margin offset, clearing the sticky header.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 20:36:46 +02:00
m038 71f8629d18 fix(scroll): add scroll-margin-top to all feed entry types
Offsets scroll targets by header height + 1rem so marker clicks land
below the sticky nav, not behind it. Applies to both .journal-post
and .entry-card (story cards).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 20:33:57 +02:00
m038 b1492918d5 feat(trip): add back-to-top button
Reuses .story-totop styles. Appears after scrolling past 80% of
viewport height, smooth-scrolls to top on click.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 20:29:07 +02:00
m038 95ea38d250 fix(feed): reduce excess spacing between entries
Removed redundant margin-bottom on .journal-post (feed gap already
separates items). Reduced padding-bottom and gap from 3rem to 2rem,
cutting the between-entry whitespace roughly in half.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 20:25:02 +02:00
m038 81be69f08d fix(photos): make PhotoSwipe background fully opaque
Prevents page dots from leaking through the semi-transparent overlay.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 20:21:33 +02:00
m038 71eaa3e788 feat(photos): adaptive aspect ratio per entry (portrait 4:5, landscape 4:3)
Portrait entries (first image taller than wide) get a 4:5 container —
Instagram's proven cap that prevents single photos dominating the screen.
Landscape entries keep 4:3. Aspect-ratio moved from slide to wrap so the
strip inherits it via height:100%.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 20:18:05 +02:00
m038 5c75f1416f feat(photos): blurred ambient fill, dots overlay, PhotoSwipe on trip + dailies
- object-fit: contain + ::before blurred background fill on all slides
- dots moved inside photo-wrap, overlaid at bottom with shadow for contrast
- arrows hidden on touch devices via @media (hover: none)
- margin-bottom increased for breathing room below photo block
- trip.html.twig brought up to parity with dailies: same structure,
  same PhotoSwipe init, same expand button, same dot overlay

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 20:09:24 +02:00
m038 3379e50503 fix(dailies): fix PhotoSwipe CSS loading + restore expand button
Move PhotoSwipe CSS from per-entry assets.addCss() (runs after head is
committed) to a single <link> tag at block start. Restore the expand
button as the reliable mobile tap target — it dispatches a synthetic
click on the visible <a> slide, which bubbles to PhotoSwipe's gallery
handler. Merge dot sync into the single module script.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 19:39:33 +02:00
m038 30c8937566 feat(dailies): replace custom lightbox with PhotoSwipe v5
Drops ~80 lines of fragile custom lightbox JS/HTML/CSS in favour of
PhotoSwipe v5 loaded from CDN. Slides are now <a> tags (the pswp-gallery
children) which always fire click events on iOS even inside scroll
containers — the root cause of the mobile tap issue. Pinch-to-zoom and
swipe-to-dismiss come for free. Dot sync kept as a separate vanilla JS
block.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 19:31:27 +02:00
m038 770a96b099 feat(dailies): 4:3 photo strip, lightbox, and dot sync
- Aspect ratio 3:2 → 4:3 (less aggressive crop; closer to phone native)
- Slides become <button> elements with data-full pointing to original image
- Tap/click any photo in the feed opens a full-screen lightbox showing the
  uncropped original; prev/next browses all feed photos; Esc/arrows/backdrop
  click close
- Dot indicator now syncs with scroll via IntersectionObserver

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 19:06:54 +02:00
m038 604ba00c70 content: set transport_mode on all central-asia-2023 entries; add plane option to blueprint
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 18:28:44 +02:00
m038 b6c9d0b2ac fix: centripetal Catmull-Rom spline to prevent overshooting near close markers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 18:20:26 +02:00
m038 51ab99b839 content: move Farewell Vodka entry between Khorog and Bukhara
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 18:10:08 +02:00
m038 2f733e5ffc chore: ignore ui-test post entries from gitignore
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 18:02:54 +02:00
m038 d7e3162f55 (Grav GitSync) Automatic Commit from m038 2026-06-21 15:51:01 +00:00
m038 8e127e7e3a (Grav GitSync) Automatic Commit from m038 2026-06-21 15:45:54 +00:00
m038 e853cb543a (Grav GitSync) Automatic Commit from m038 2026-06-21 15:40:39 +00:00
m038 e29953ab90 (Grav GitSync) Automatic Commit from m038 2026-06-21 15:38:11 +00:00
m038 366974475f (Grav GitSync) Automatic Commit from m038 2026-06-21 15:37:57 +00:00
m038 fa29888578 chore: ignore git-sync.yaml and security.yaml
Both files are server-specific: git-sync.yaml contains an encrypted
token, security.yaml holds Grav nonces/salts. Neither should roam
across environments.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 17:25:22 +02:00
m038 31f3c6fb2f fix: resolve AX6/AX7 a11y violations
- gpx-manager: raise th color #666→#999 (6.9:1 contrast on dark bg)
- gpx-manager: raise .gpx-delete text #c0392b→#e07070 (6.2:1 contrast)
- gpx-manager: add visible label text 'Choose GPX file' to file input
- snap-gallery: add tabindex=0 to .pgallery__frame for keyboard scrollability

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WPJztrVGbwic2xTG7G9fjM
2026-06-21 17:24:29 +02:00
m038 936662e35c chore: update admin password
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 16:17:08 +02:00
m038 a440583691 content: add home page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 15:10:51 +02:00
m038 6486d377b2 chore: commit config drift from CMS usage
accounts/mischa.yaml: pagesViewMode set to tree via Admin UI
config/site.yaml: active_trip updated from demo to us-canada-mex-2024

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 15:10:49 +02:00
m038 6c842ebe7f content: apply enrichment data to central-asia-2023 entries
Fills in lat, lng, weather_temp_c, and weather_desc for all 23 entries.
Completes the enrichment committed in 3f53bf5 which only renamed folders.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 15:10:45 +02:00
m038 89c9771a84 chore: untrack third-party plugins from user repo
All plugins except cache-on-save and story-blocks are managed by GPM
and server-install.sh — they should not be in version control.
Fixes .gitignore to plugins/* with explicit allows for custom plugins only.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 15:10:33 +02:00
m038 89ae41d9ec Ignore italy-2026-demo in user repo
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 15:01:52 +02:00
m038 3983615c99 Remove demo content from user repo
Demo trip italy-2026-demo was accidentally committed; it should only
exist in docs/demo/ and be loaded locally via make demo-load.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vgmzx8VTTTmCskSpQtsLTr
2026-06-21 15:01:39 +02:00
5348 changed files with 1142 additions and 968005 deletions
+7 -2
View File
@@ -1,4 +1,9 @@
/plugins/
!/plugins/
/plugins/*
!/plugins/.gitkeep
!/plugins/cache-on-save/
!/plugins/story-blocks/
/data/
/pages/01.trips/italy-2026-demo/
/pages/02.post/*ui-test*/
/config/plugins/git-sync.yaml
/config/security.yaml
+6 -3
View File
@@ -1,11 +1,11 @@
login: mischa
state: enabled
title: Mischa
title: ''
email: mischa@gorinskat.nl
fullname: Mischa
hashed_password: $2y$10$gAhnWvgzquwC0Vg5vwSJee22eO5kLxphS4RiqMZv9nf61kX.96XW6
hashed_password: $2y$10$xyV7bAUWEo75K6LbatUuYe/6x2Tj9nT6YnIjaDvESAhU2hJ7tjG2.
language: en
modified: 1781874349
modified: 1782051328
admin_next:
preferences:
pluginsViewMode: cards
@@ -13,6 +13,9 @@ admin_next:
accentHue: 271
accentSaturation: 91
fontFamily: inter
pagesViewMode: tree
content_editor: ''
groups: { }
access:
admin:
login: true
-1
View File
@@ -1 +0,0 @@
salt: HlC0NrX9QsYq1S
+3 -4
View File
@@ -1,10 +1,9 @@
title: 'Into the East'
description: 'A travel blog by Mischa'
author:
name: Mischa
email: mischa@gorinskat.nl
taxonomies: [category, tag]
metadata:
description: 'Into the East — travel journal'
active_trip: /trips/italy-2026-demo
travelling: true
description: 'A travel blog by Mischa'
active_trip: /trips/us-canada-mex-2024
travelling: false
@@ -4,12 +4,12 @@ date: '2023-08-28 07:49'
template: entry
published: true
hero_image: 'photo-1.png'
lat: ''
lng: ''
lat: '52.5200'
lng: '13.4050'
location_city: 'Berlin'
location_country: 'Germany'
weather_temp_c: ''
weather_desc: ''
weather_temp_c: '24'
weather_desc: 'sunny'
---
Welcome to my picture diary of my 2023 adventure through Central Asia! Get ready for a visual journey through breathtaking landscapes and unforgettable moments! Join me as I explore the winding roads of Kazakhstan, Kyrgyzstan, Tajikistan, and Uzbekistan. Follow along as I share one picture per day, capturing unique experiences and adding stories to bring them to life. Get inspired and come along on this exciting adventure with me!
@@ -2,14 +2,15 @@
title: 'Last Beer Before the Foreign Land'
date: '2023-08-29 10:29'
template: entry
transport_mode: train
published: true
hero_image: 'photo-1.jpg'
lat: ''
lng: ''
lat: '52.36402'
lng: '13.50745'
location_city: 'Berlin'
location_country: 'Germany'
weather_temp_c: ''
weather_desc: ''
weather_temp_c: '24'
weather_desc: 'sunny'
---
Today the trip has become a reality, passed through security and waiting at the gate... A strange feeling, the months of anticipation are changing into a reality that so far only has lived in fantasy and bank statements... A last German beer to sooth that anxious feeling of knowing that for the next few months everything will be different!
@@ -2,14 +2,15 @@
title: 'The UAZ Buchanka Counter Begins'
date: '2023-08-30 18:06'
template: entry
transport_mode: plane
published: true
hero_image: 'photo-1.jpg'
lat: ''
lng: ''
location_city: ''
location_country: ''
weather_temp_c: ''
weather_desc: ''
lat: '51.140108'
lng: '71.429747'
location_city: 'Astana'
location_country: 'Kazakhstan'
weather_temp_c: '15'
weather_desc: 'rainy'
---
A simple van, quite boring even if I'd say to myself after all I've seen today. Though this is not just some van, it's a UAZ "Buchanka". I do have a special and weird relationship with these cars, the little UAZ 469 brother as well. Within first 50 meters of my Hostel i saw this one and i couldn't resist capturing it on my camera... so far this is Number 2 i spotted during my trip! I'll keep the counter running...
@@ -2,14 +2,15 @@
title: 'Baiterek: Bird of Happiness in Astana'
date: '2023-08-31 16:45'
template: entry
transport_mode: walking
published: true
hero_image: 'photo-1.jpg'
lat: ''
lng: ''
lat: '51.128246'
lng: '71.430466'
location_city: 'Astana'
location_country: 'Kazakhstan'
weather_temp_c: ''
weather_desc: ''
weather_temp_c: '15'
weather_desc: 'cloudy'
---
After finishing my Beshbarmak I was thinking it would be today's picture. Even though it's Kazakhstans main dish, it doesn't represent my day which was about Kazakhstans history and culture ... So.... Бәйтерек (Baïterek) a monument built to support the moving of the capital from Almaty to Astana. It symbolizes the mythical tree of live in which Samruk, the bird of happiness, has laid it's egg. A new beginning for Kazakhstan after its independence.
@@ -2,14 +2,15 @@
title: 'Doshirak and Politics on the Night Train'
date: '2023-09-02 15:47'
template: entry
transport_mode: train
published: true
hero_image: 'photo-1.jpg'
lat: ''
lng: ''
lat: '43.2220'
lng: '76.8512'
location_city: 'Almaty'
location_country: 'Kazakhstan'
weather_temp_c: ''
weather_desc: ''
weather_temp_c: '25'
weather_desc: 'sunny'
---
Last night I took a 15 hour train ride from Astana to Almaty. A modern train with a bar and self prepared Doshirak noodles.
@@ -2,14 +2,15 @@
title: 'Plov and Street Art in Almaty'
date: '2023-09-03 16:38'
template: entry
transport_mode: train
published: true
hero_image: 'photo-1.jpg'
lat: ''
lng: ''
lat: '43.2220'
lng: '76.8512'
location_city: 'Almaty'
location_country: 'Kazakhstan'
weather_temp_c: ''
weather_desc: ''
weather_temp_c: '24'
weather_desc: 'sunny'
---
Almaty, a wonderful city in which I realise will not be able to spend enough time. Its more modern (it has so many bikelanes), has more character than Astana, but also is on the border with beautiful nature...
@@ -2,14 +2,16 @@
title: 'Rain in Charyn Canyon, Manti for Dinner'
date: '2023-09-04 15:50'
template: entry
transport_mode: car
published: true
hero_image: 'photo-1.jpg'
lat: ''
lng: ''
location_city: ''
location_country: ''
weather_temp_c: ''
weather_desc: ''
hero_image: photo-1.jpg
lat: '43.35102'
lng: '79.08010'
location_city: 'Charyn Canyon'
location_country: Kazakhstan
weather_temp_c: '18'
weather_desc: 'cloudy with showers'
featured: true
---
The adventure started, a big jeep, few adventurous tourists and our guide Aybek.
@@ -2,14 +2,15 @@
title: 'Kurt, Kumis and a UAZ Dream Ride'
date: '2023-09-05 15:50'
template: entry
transport_mode: car
published: true
hero_image: 'photo-1.jpg'
lat: ''
lng: ''
location_city: ''
location_country: ''
weather_temp_c: ''
weather_desc: ''
lat: '43.068370'
lng: '78.412857'
location_city: 'Kaindy / Kolsai'
location_country: 'Kazakhstan'
weather_temp_c: '20'
weather_desc: 'sunny'
---
The planned focus of today was nature! Seeing Kaindy and Kolsai lakes. They are amazingly beautiful.
@@ -2,14 +2,15 @@
title: 'First Hike Up Toward Ala Kol'
date: '2023-09-07 17:00'
template: entry
transport_mode: car
published: true
hero_image: 'photo-1.jpg'
lat: ''
lng: ''
lat: '42.4900'
lng: '78.3936'
location_city: 'Karakol'
location_country: 'Kyrgyzstan'
weather_temp_c: ''
weather_desc: ''
weather_temp_c: '0'
weather_desc: 'partly cloudy'
---
Via the border at Kegen, I made it to Karakol. Ala Kol is the most beautiful of all Kyrgyz lakes! In a good mood I set out and after taking a drive with a UAZ 469B 😎 I started going up. 500 meters upward and 15km further I found a camp to spend the night. Next I planned to go to the lake and take the same route back. This track is not for inexperienced hikers. After a 300 meter incline I decided to go back and not get stuck with 0 energy. Im proud of my achievement as a first time hiker!
@@ -2,14 +2,15 @@
title: 'Tea Trails and No Seatbelts in Kyrgyzstan'
date: '2023-09-10 06:16'
template: entry
transport_mode: bus
published: true
hero_image: 'photo-1.jpg'
lat: ''
lng: ''
location_city: ''
location_country: ''
weather_temp_c: ''
weather_desc: ''
lat: '42.876640'
lng: '74.603745'
location_city: 'Bishkek'
location_country: 'Kyrgyzstan'
weather_temp_c: '16'
weather_desc: 'partly cloudy'
---
Kyrgyzstan is a beautiful country with many mountains, beautiful lakes, friendly people and no seatbelts. I definitely did not get a full grasp of it all... Just a brief morning in Karakol half a day in Bishkek, room with a view, and a quick flight to OSU for just half an evening... I did however start to explore the many different teas they serve here. Now the adventure continuous toward the Pamir mountains in Tajikistan!
@@ -2,14 +2,15 @@
title: "Stuck in No Man's Land at 4655m"
date: '2023-09-18 03:20'
template: entry
transport_mode: car
published: true
hero_image: 'photo-1.jpg'
lat: ''
lng: ''
location_city: ''
location_country: ''
weather_temp_c: ''
weather_desc: ''
lat: '39.384414'
lng: '73.322529'
location_city: 'Akbaital Pass'
location_country: 'Tajikistan'
weather_temp_c: '16'
weather_desc: 'partly cloudy'
---
10-09 - The day marks an end to being a guest in Kyrgyzstan and a beginning of being welcomed by Tajik hospitality. We drive from Osh to Karakul (TJ) and climb from about 1000 meters to nearly 4000 meters altitude. We cross the Akbaital Pass (4655m) and try to leave Kyrgyzstan.
@@ -2,14 +2,15 @@
title: 'Black Water Lake on the Pamir Highway'
date: '2023-09-19 05:50'
template: entry
transport_mode: car
published: true
hero_image: 'photo-1.jpg'
lat: ''
lng: ''
location_city: ''
location_country: ''
weather_temp_c: ''
weather_desc: ''
lat: '39.01250'
lng: '73.55978'
location_city: 'Karakul'
location_country: 'Tajikistan'
weather_temp_c: '15'
weather_desc: 'windy'
---
10-09 - In the middle of no mans land, right on the actual border, our new driver was waiting and we started our tour through Tajikistan. The epic and harsh nature of the Pamirs is spectacular and makes it unimaginable how people can survive here, especially In winter. We ended in Karakul, not Karakol where I was before. Kara means black, kul means lake. The lake has beautiful.colours depending on the sunlight, but its name comes from the fact thats its fed with ground (black) water.
@@ -2,14 +2,15 @@
title: 'Warm Soup in a Village of Hundreds'
date: '2023-09-19 06:04'
template: entry
transport_mode: car
published: true
hero_image: 'photo-1.jpg'
lat: ''
lng: ''
location_city: ''
location_country: ''
weather_temp_c: ''
weather_desc: ''
lat: '37.755579'
lng: '73.271513'
location_city: 'Alichur'
location_country: 'Tajikistan'
weather_temp_c: '15'
weather_desc: 'windy'
---
11-09 - In this village live just a few hundred people and there is a small military base. There is a school and a mosque, a very hard to find shop (we did not succeed) and some water wells. People drive old, rugged, Soviet UAZs, its the only affordable machine that can survive the rocky roads.
@@ -2,14 +2,15 @@
title: 'Afghanistan Just Across the Wakhan River'
date: '2023-09-23 08:41'
template: entry
transport_mode: car
published: true
hero_image: 'photo-1.jpg'
lat: ''
lng: ''
location_city: ''
location_country: ''
weather_temp_c: ''
weather_desc: ''
lat: '37.032071'
lng: '72.630602'
location_city: 'Zong'
location_country: 'Tajikistan'
weather_temp_c: '28'
weather_desc: 'sunny'
---
PMU14-9 - Zong (what a name, huh). We descended from the harsh Pamir Plateau into the greener Wakhan valley. Epic views and epic roads (our driver is a hero) we past in our study Toyota Landcruiser. To finally meet another mysterious land, that became an obsession in our group...
@@ -2,14 +2,15 @@
title: 'The Night the Beer Finally Arrived'
date: '2023-09-23 08:29'
template: entry
transport_mode: car
published: true
hero_image: 'photo-1.jpg'
lat: ''
lng: ''
location_city: ''
location_country: ''
weather_temp_c: ''
weather_desc: ''
lat: '37.755660'
lng: '73.271591'
location_city: 'Alichur'
location_country: 'Tajikistan'
weather_temp_c: '15'
weather_desc: 'windy'
---
PMU13-9 - Alichur, slightly less harsh than Karakul, there is grass for yaks and sheep.
@@ -1,15 +1,16 @@
---
title: "Farewell Vodka Under the World's Tallest Flag"
date: '2023-09-20 16:19'
date: '2023-10-01 18:00'
template: entry
transport_mode: car
published: true
hero_image: 'photo-1.jpg'
lat: ''
lng: ''
lat: '38.5598'
lng: '68.7870'
location_city: 'Dushanbe'
location_country: 'Tajikistan'
weather_temp_c: ''
weather_desc: ''
weather_temp_c: '28'
weather_desc: 'sunny'
---
Dushanbe, a warm welcome back into civilization after the Pamirs. Another kind of weird modern city, the central park is beautiful and they have a huge flag. The pole was the largest in the world, till recently. Its over 150 meters and the flag weighs more than 700 kgs.
@@ -2,14 +2,15 @@
title: 'Hot Springs and a Pamiri Homestay'
date: '2023-10-01 10:40'
template: entry
transport_mode: car
published: true
hero_image: 'photo-1.jpg'
lat: ''
lng: ''
location_city: ''
location_country: ''
weather_temp_c: ''
weather_desc: ''
lat: '36.983527'
lng: '72.264250'
location_city: 'Ishkashim / Bibi Fatima'
location_country: 'Tajikistan'
weather_temp_c: '18'
weather_desc: 'sunny'
---
PMU15-9 - Along the way we visited a traditional Pamiri house. We've slept in quite a few with a similar design. The guide gave us insights in how people lived. Usually three/four generations shared a single house with usually two rooms. They were multipurpose rooms for cooking, living and sleeping.
@@ -2,14 +2,15 @@
title: 'What Is Normal? Reflections from Khorog'
date: '2023-10-01 10:51'
template: entry
transport_mode: car
published: true
hero_image: 'photo-1.jpg'
lat: ''
lng: ''
location_city: ''
location_country: ''
weather_temp_c: ''
weather_desc: ''
lat: '37.49046'
lng: '71.53921'
location_city: 'Khorog'
location_country: 'Tajikistan'
weather_temp_c: '25'
weather_desc: 'sunny'
---
Recap - When travelling to new places, I always wonder what is their "normal" and how much do i perceive this as "weird".
@@ -1,15 +1,16 @@
---
title: 'Millionaires and Minarets in Bukhara'
date: '2023-09-23 08:14'
date: '2023-10-02 00:00'
template: entry
transport_mode: train
published: true
hero_image: 'photo-1.jpg'
lat: ''
lng: ''
location_city: ''
location_country: ''
weather_temp_c: ''
weather_desc: ''
lat: '39.775957'
lng: '64.416693'
location_city: 'Bukhara'
location_country: 'Uzbekistan'
weather_temp_c: '25'
weather_desc: 'sunny'
---
In Uzbekistan everyone can be a millionaire, it only costs about €80. A handmade magnet costs 25k and a beer 30k. Definitely the hardest currency to convert in my mind this trip.
@@ -2,14 +2,15 @@
title: 'Four Weeks in Central Asia, Barely Enough'
date: '2023-10-03 07:58'
template: entry
transport_mode: train
published: true
hero_image: 'photo-1.jpg'
lat: ''
lng: ''
location_city: 'Tbilisi'
location_country: 'Georgia'
weather_temp_c: ''
weather_desc: ''
lat: '40.37660'
lng: '49.84752'
location_city: 'Baku'
location_country: 'Azerbaijan'
weather_temp_c: '24'
weather_desc: 'sunny'
---
The holiday adventure is over. 4 weeks of central Asia was just enough to scratch the surface on most countries and I definitely went to go back!
@@ -2,14 +2,15 @@
title: "Timur's Samarkand and a Badly Parked Truck"
date: '2023-10-03 07:38'
template: entry
transport_mode: train
published: true
hero_image: 'photo-1.jpg'
lat: ''
lng: ''
location_city: ''
location_country: ''
weather_temp_c: ''
weather_desc: ''
lat: '39.65841'
lng: '66.98542'
location_city: 'Samarkand'
location_country: 'Uzbekistan'
weather_temp_c: '25'
weather_desc: 'sunny'
---
Samarkand, Amir Timur made this city great again as the capital of his vast empire. It sparked a wave of Islamic art inspired by the styles in Persia. The buildings are beautifully restored, sometimes with a little imagination. It gives however a great feeling of how these buildings must have looked like, simply beautiful!
@@ -2,14 +2,15 @@
title: 'Hunting the Mother of Georgia from Above'
date: '2023-10-18 07:38'
template: entry
transport_mode: plane
published: true
hero_image: 'photo-1.jpg'
lat: ''
lng: ''
lat: '41.6938'
lng: '44.8015'
location_city: 'Tbilisi'
location_country: 'Georgia'
weather_temp_c: ''
weather_desc: ''
weather_temp_c: '20'
weather_desc: 'partly cloudy'
---
Past weekend. I was fit enougto exploe more of Tbilisi. A hike in the nearby mountains gave great views of the city and the backside of the Mother of Georgia statue. So I went on a mission to capture her majestic front.... not so easy in a hily city, but I managed.
+1
View File
@@ -6,3 +6,4 @@ date_start: '2025-10-11'
date_end: '2025-10-16'
cover_image: ''
---
@@ -1,16 +0,0 @@
---
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.
@@ -1,30 +0,0 @@
---
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.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 419 KiB

@@ -1,33 +0,0 @@
---
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.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 190 KiB

@@ -1,29 +0,0 @@
---
title: 'The Road to Montalcino'
date: '2026-09-06'
location_name: Montalcino
location_country: Italy
lat: 43.057
lng: 11.489
hero_image: hero.jpg
hero_alt: Vine rows climbing toward a hilltop tower in early morning light, mist in the valley below
published: true
---
The climb to Montalcino starts gently enough. A white gravel road, the kind that shows up cream in photographs and grey in real life, curling upward through the first vineyards. The gradient is polite for about twenty minutes and then it isn't. By the time the tower came into view above the tree line we had stopped pretending to chat and were just breathing.
[full-bleed image="photo-2.jpg" caption="The valley floor from halfway up — on a clear morning you can see all the way to Monte Amiata" alt="Wide view of Tuscan valley with vineyards in foreground and hills receding into morning haze" credit="Day 6, 07:40"]
Brunello country. The vineyards up here have a precision to them that you don't get further down — rows tighter, stakes straighter, the ground between them weeded with what looks like obsessive care. These are grapes worth taking seriously and the farmers treat them accordingly. We rode between the rows for a while, the vines overhead, dew still on the leaves.
[image-caption image="photo-1.jpg" caption="Sangiovese Grosso — the only grape permitted in a Brunello. The bunches are small and tight, almost black by September." alt="Close-up of dark grape clusters on a vine with green leaves, morning light filtering through" credit="Canalicchio di Sopra vineyard" width="column"]
The town at the top is small enough that you can walk end to end in fifteen minutes, but it has everything: a bar with a terrace facing southwest, a shop selling nothing but wine, and an enoteca run by a man who spoke no English and didn't need to. He poured three glasses without being asked and set down a plate of bread and something salty. We drank slowly. The view from the terrace was the entire reason the town exists in the position it does.
[image-caption image="hero.jpg" caption="Montalcino from the south approach — the fortress is eleventh century; the view it commands is the reason it was built exactly here." alt="Hilltop town of Montalcino with medieval fortress visible above terracotta rooftops, vineyards in foreground" credit="09:15" width="full"]
[pull-quote]
Every Tuscan hill town has a reason to be where it is. Montalcino's reason is military, but after a thousand years all that fortification has become a very good wine cellar.
[/pull-quote]
The descent was fast. White gravel becomes tarmac becomes a smooth fast road through the valley and you can carry almost everything you earned on the way up. We were back at camp by noon, which felt like cheating. In the afternoon I slept for two hours in the shade of an olive tree, which did not feel like cheating at all.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-8
View File
@@ -1,8 +0,0 @@
---
title: 'Tuscany 2026'
template: trip
date: '2026-09-01'
date_start: '2026-09-01'
date_end: '2026-09-08'
cover_image: ''
---
@@ -4,12 +4,12 @@ date: '2024-05-28 07:03'
template: entry
published: true
hero_image: 'photo-1.jpg'
lat: ''
lng: ''
location_city: ''
location_country: ''
weather_temp_c: ''
weather_desc: ''
lat: '45.5285'
lng: '13.5680'
location_city: Piran
location_country: Slovenia
weather_temp_c: '21'
weather_desc: sunny
---
A sunny day in Piran. We drove from Ljubljana through the beautiful Slovenian countryside. The more west we went, the more Mediterranean the landscape felt. Piran is a cute, Mediterranean harbor town, with little streets, squares and no cars. The view from the old fortification walls was great and the climb in the warm weather gave us a sense of accomplishment which we rewarded with a well deserved ice cream.
@@ -0,0 +1,11 @@
---
title: 'The Journey'
template: dailies
content:
items: '@self.children'
order:
by: date
dir: desc
filter:
published: true
---
+8
View File
@@ -0,0 +1,8 @@
---
title: 'Slovenia 2024'
template: trip
date: '2024-05-28'
date_start: '2024-05-28'
date_end: '2024-05-28'
cover_image: ''
---
+2 -2
View File
@@ -1,8 +1,8 @@
---
title: 'Northern America 2024'
template: trip
date: '2024-05-28'
date_start: '2024-05-28'
date: '2024-07-21'
date_start: '2024-07-21'
date_end: '2024-08-07'
cover_image: ''
---
+11
View File
@@ -0,0 +1,11 @@
---
title: 'Into the East'
visible: false
routable: true
routes:
default: /
---
Dispatches from foreign lands and places closer by. A few moments worth remembering, collected here while I'm between trips.
-- Mischa
-237
View File
@@ -1,237 +0,0 @@
# v3.3.0
## 09/14/2023
1. [](#improved)
- Added support multiple slug_fields to form unique slug, either as comma separated list or as array. Many thanks to walkload for the [pull request](https://github.com/bleutzinn/grav-plugin-add-page-by-form/pull/65)
# v3.2.0
## 02/17/2023
1. [](#bugfix)
- Fixed a problem when `pagefrontmatter.content` was set (issue [#63](https://github.com/bleutzinn/grav-plugin-add-page-by-form/issues/63))
1. [](#bugfix)
- Fixed incorrect handling of using `pageconfig.parent: /` (issue [#59](https://github.com/bleutzinn/grav-plugin-add-page-by-form/issues/59))
1. [](#improved)
- Added composer
# v3.1.0
## 12/13/2022
1. [](#improved)
- Tested with PHP version 8 (8.1.10)
- Fixed some incorrect variable types in PHPDoc Comments
1. [](#bugfix)
- Changed the declaration of the function `sanitize` to "static". [#62](https://github.com/bleutzinn/grav-plugin-add-page-by-form/issues/62)). Thanks to masetto for raising this issue.
# v3.0.4
## 03/28/2022
1. [](#bugfix)
- A title containing diacritic characters prevented a form submit (issues [#56](https://github.com/bleutzinn/grav-plugin-add-page-by-form/issues/56) and [#60](https://github.com/bleutzinn/grav-plugin-add-page-by-form/issues/60)). Thanks goes to davay for the pull request.
# v3.0.3
## 05/29/2020
1. [](#bugfix)
- Fixed not handling overwrite_mode setting properly, issue [#54](https://github.com/bleutzinn/grav-plugin-add-page-by-form/issues/54), thanks to mooomooo for reporting and testing
# v3.0.2
## 05/17/2020
1. [](#improved)
- Yet another release. This time just to synchronize the versioning number in 'bleuprints.yaml' and this Changelog. For interesting changes see v3.0.0.
# v3.0.1
## 05/17/2020
1. [](#improved)
- Removed a newline between the version number and the date in this Changelog file in an attempt to restore the correct display of this file in the Grav repository
# v3.0.0
## 05/03/2020
1. [](#new)
- New 'overwrite_mode' option 'edit' allows for editing a page. Note: yet undocumented.
- Removing upload files is now handled
1. [](#bugfix)
- Switched to Laravel str_slug function to remedy problems with hyphens on some Windows systems
1. [](#improved)
* (Possibly breaking change:) Changed uploaded files data structure from numeric array to associative array
* (Possibly breaking change:) Changed default setting of 'physical_template_name' to 'true'
* Minimum Grav version is set to 1.6
# v2.4.1
## 04/12/2020
1. [](#bugfix)
* Fixed a bug ([issue #52](https://github.com/bleutzinn/grav-plugin-add-page-by-form/issues/52)) where an empty value for the 'slug' variable would delete folder(s). Thanks to [anton-mellit](https://github.com/anton-mellit) for reporting this.
# v2.4.0
## 01/28/2020
1. [](#new)
* Added a new config variable 'physical_template_name' to make using the new page template name as the new page's file name optional.
1. [](#improved)
* Removed the Changelog entry that was included in [https://github.com/bleutzinn/grav-plugin-add-page-by-form/pull/47](PR #47) from this file as it was not in the Grav Changelog format and prevented changes showing up correctly in the Grav Plugins download section.
# v2.3.5
## 01/25/2020
1. [](#new)
* Add option to suppress loading of simpleMDE assets (reduces overhead if its known it will not be used)
# v2.3.4
## 01/20/2020
1. [](#bugfix)
* Prepared a new release to mainly consolidate the fix "Use moveTo method not native copy to move uploaded files to final destination" and to bring this changelog format back in line with Grav requirements. Thanks to Dave Nichols (pd-giz-dave).
1. [](#improved)
* Also included the ability to use the new page template name as the new page's folder name. Thanks to Dave Nichols (pd-giz-dave).
# v2.3.3
## 12/05/2019
1. [](#bugfix)
* Fixed inconsistancies in version numbering which prevented the addition of the latest updates in the Grav Plugin repository.
# v2.3.2
## 11/24/2019
1. [](#bugfix)
* Prepared a new release to fix a bug in version numbering. The letter "v" appears to be case sensitive. The versions 2.3.0 and 2.3.1 were tagged with a capital "V" as V2.3.0 and V2.3.1 respectively. Previous versions were tagged using a lowercase "v". This difference causes the Grav Repository to think these are two different plugins.
# v2.3.1
## 10/10/2019
1. [](#bugfix)
* Fixed a subsequent failure to save file uploads to new page folder (form field File with `destination: @self`) introduced with Grav version 1.6.11 ([issue #44](https://github.com/bleutzinn/grav-plugin-add-page-by-form/issues/44)). Thanks goes to mahagr for tips and to tranduyhung for the fix itself.
# v2.3.0
## 06/06/2019
1. [](#bugfix)
* Fixed the failure to save file uploads to new page folder (form field File with `destination: @self`) introduced with Grav version 1.6 ([issue #40](https://github.com/bleutzinn/grav-plugin-add-page-by-form/issues/40))
# v2.2.0
## 04/15/2018
1. [](#new)
* Added support for `process.redirect: @self-admin` ([issue #13](https://github.com/bleutzinn/grav-plugin-add-page-by-form/issues/31))
1. [](#improved)
* Fixed a problem with uploading files
# v2.1.0
## 09/18/2017
1. [](#new)
* Added support for taxonomy types and tags
# v2.0.0
## 06/18/2017
1. [](#new)
* Added support for multiple textarea editors ([issue #21](https://github.com/bleutzinn/grav-plugin-add-page-by-form/issues/21))
* Added support for `process.redirect: @self` ([issue #23](https://github.com/bleutzinn/grav-plugin-add-page-by-form/issues/23))
* Added the `overwrite_mode` configuration frontmatter variable
* Added the `subroute` configuration frontmatter variable
* Added the `slug_field` configuration frontmatter variable
* Added filename sanitizing of uploaded files
1. [](#improved)
* In the form page frontmatter configuration variables are separated from variables which main purpose it is to get passed on to the new page
* Uploaded file properties are now included in the new page frontmatter
* Improved safe slug generator
* Removed "use editor" option from `blueprints.yaml` (to allow [issue #21](https://github.com/bleutzinn/grav-plugin-add-page-by-form/issues/21))
* Extended `blueprints.yaml` to set "fallback" configuration values
1. [](#bugfix)
* Fixed an issue with form pages outside the web root ([issue #20](https://github.com/bleutzinn/grav-plugin-add-page-by-form/issues/20))
* Fixed a problem that prevented having different destinations for file uploads
# v1.4.2
## 03/22/2017
1. [](#new)
* Cleaned up code for release.
# v1.4.1
## 02/16/2017
1. [](#improved)
* Simplified YAML frontmatter formatting as suggested in
https://github.com/getgrav/grav/issues/1287#issuecomment-279965492
# v1.4.0
## 02/12/2017
1. [](#new)
* Added the ability to include the File field in the form. When `destination` is `@self` uploaded files are stored in the new page folder.
# v1.3.2
## 01/31/2017
1. [](#improved)
* Removed the spyc.php class dependency; the page creation and YAML frontmatter handling is now done "the Grav way".
# v1.3.1
## 01/15/2017
1. [](#improved)
* Added jQuery as an asset.
# v1.3.0
## 12/31/2016
1. [](#new)
* Added the SimpleMDE Markdown Editor.
# v1.2.2
## 12/29/2016
1. [](#improved)
* Removed note about the (previous) test release in the ReadMe.
# v1.2.1
## 12/29/2016
1. [](#improved)
* Improved the usage explanation in the ReadMe.
* Removed debug messages.
# v1.2.0
## 12/23/2016
1. [](#improved)
* Improved new page route handling.
# v1.1.1
## 11/17/2016
1. [](#improved)
* Removed dependency of PECL YAML function yaml_emit() in favor of using vendor/spyc.php class.
# v1.1.0
## 11/16/2016
1. [](#new)
* Settings in the pagefrontmatter block in the form page frontmatter now are merged with values from form fields. Form field values ovverride the pagefrontmatter settings.
# v1.0.0
## 11/15/2016
1. [](#new)
* Added an extra form field: 'author'
* Added copying an (optional) frontmatter block from the form page frontmatter to the newly added page's frontmatter
# v0.2.0
## 11/13/2016
1. [](#new)
* Plugin name changed to Add Page By Form (add-page-by-form)
* Added timestamp as date in page header (date format is taken from plugin config)
* Pages with identical titles are saved by adding a incremental number to the page slug (e.g. 'my-page\_2', 'my-page\_2', etc.)
* Added error handling
# v0.1.0
## 11/08/2016
1. [](#new)
* ChangeLog started...
-21
View File
@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2016 Ron Wardenier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-444
View File
@@ -1,444 +0,0 @@
# Add Page By Form Plugin
The **Add Page By Form** Plugin is for [Grav CMS](http://github.com/getgrav/grav). It allows users to add a new page by filling in a form.
This plugin uses the possibilities of [custom frontmatter](https://learn.getgrav.org/content/headers#custom-page-headers). By setting your own variables in the form page frontmatter a priori and optionally letting users override these variable values by filling in corresponding form fields you can transport these data into the new page frontmatter.
The passing on of both the default settings and form field values entered by the end user to the new page frontmatter makes for an extremely configurable solution.
By mixing default settings and configuring the page form you can to a large extent control the appearence and behaviour of the newly added page by using the frontmatter variables present in the new page in a Twig template.
For example, a new page can act as a new blog post simply by setting the appropriate template variable in the form page definition (with the AntiMatter theme, this is `template: item`). That template value is inserted in the new page frontmatter and, so, will be used by Grav to display the new page.
## Security Warning
Allowing anonymous visitors to create pages is a potential website security risk. It is **strongly advised** to use the [Grav Login Plugin](https://github.com/getgrav/grav-plugin-login) or the [Private Grav Plugin](https://github.com/Diyzzuf/grav-plugin-private) **to restrict the page creation to logged in users only**.
This plugin itself does not provide any security measures. Please take this in consideration before using this plugin.
## Installation and Configuration
Typically the plugin should be installed via [GPM](http://learn.getgrav.org/advanced/grav-gpm) (Grav Package Manager):
```
$ bin/gpm install add-page-by-form
```
Alternatively it can be installed via the [Admin Plugin](http://learn.getgrav.org/admin-panel/plugins).
Another option is to manualy install the plugin by [downloading](https://github.com/bleutzinn/grav-plugin-add-page-by-form/archive/master.zip) the plugin as a zip file. Copy the zip file to your `/user/plugins` directory, unzip it there and rename the folder to `add-page-by-form`.
### Configuration Defaults
Here is the default configuration in the configuration file `add-page-by-form.yaml` plus an explanation of the settings:
```yaml
enabled: true
date_display_format: 'd-m-Y g:ia'
default_title: 'My New Page'
default_content: 'No content.'
overwrite_mode: false
include_username: false
auto_taxonomy_types: false
use_editor_class: true
physical_template_name: true
```
- `enabled` determines whether the plugin is active or not;
- `date_display_format` sets a default date and time format;
- `default_title` will be used as a fallback for the new page title when no other value is set;
- `default_content` will be used as the page content for the new page when no other value is set;
- `include_username` sets whether or not the username of the currently logged in frontend user is included in the new page frontmatter;
- `overwrite_mode` determines how to act when a page with the same name or slug already exists;
- `auto_taxonomy_types` saves any new taxonomy types that were input by the user to the site configuration file `site.yaml`;
- `use_editor_class` determines whether or not to change a textarea field into a Markdown editor when the texture field includes the class "editor" (`classes: editor`);
- `physical_template_name` should normally not be used. For more information see the description in the section 'pageconfig' block variables.
### Customizing the default configuration
To keep your custom configuration when updating the plugin you need to use a configuration file which is stored in the `user/config/plugins` folder.
Simply edit the plugin options in the Admin panel and the changes will be saved to the configuration file in that location. If you don't use the Admin panel, copy the `add-page-by-form.yaml` default file to your `user/config/plugins` folder and use that copy to change configuration settings.
## Usage
Using this plugin requires:
- a normal page containing a Grav Form with a unique name that starts with "add_page";
- optionally, but required for usefulness, one or two blocks of extra page frontmatter variables in that page being:
- a 'pageconfig' block with variables that are used in the new page creation process;
- a 'pagefrontmatter' block with variables that are passed on to the new page frontmatter and can be processed by Twig along the way.
### Modifying frontmatter variables
Every frontmatter variable value can be changed by the user when an input field with the same name as the variable is included in the form.
The basic method of modifying is overriding or replacing an initial value. An extreme case of overriding a variable which is quite uncommon but illustrates the process well:
1. `overwrite_mode` is set in `add-page-by-form.yaml`
2. and can be changed in the Plugin configuration in the Admin Panel
3. can be set in the `pageconfig` block
4. and finally may be changed again by the end user when the form contains an `overwrite_mode` field.
### Page Headers / Frontmatter
This plugin makes extensive use of [Custom Page Headers](https://learn.getgrav.org/content/headers#custom-page-headers). Unfortunately the Grav documentation mixes the terms "frontmatter", "page headers" and simply "headers". This may be confusing at first. They [all](https://learn.getgrav.org/content/headers) refer to the optional top part of a Grav page which contains data in [YAML syntax](https://learn.getgrav.org/advanced/yaml).
## Form page Frontmatter
The frontmatter in the form page and the way it is handled by the plugin is where the flexibility of this plugin originates.
The form page frontmatter is divided into three sections or blocks:
1. So called 'root level' variables are intended to act upon the form page itself. They are not passed on to the new page;
2. the `pageconfig` block contains variables that are used by the plugin in the new page creation process and do get passed on to the new page frontmatter;
3. the `pagefrontmatter` block holds all other variables that must be passed on to the new page frontmatter.
### Root level variables
In the examples above the root level configuration options are:
- `title` sets the title of the page containing the form;
- `template: form` activates the form on this page (not required when the form page is named `form.md`);
- `form` defines the form.
From version 2, the use of `parent` in the, what is now called, root level block is deprecated. It is however still supported for backwards compatibility.
### 'pageconfig' block variables
In the optional pageconfig block you can set these, and only these, variables (other variables will be ignored):
- `parent` sets the parent page for the new page. This variable may be an absolute route (for example `parent: /user_contributions`) or a relative route (e.g. `parent: articles`. In case of an absolute route this route starts from the pages root. A relative route is regarded to start from the form page, so the new page will be a child page of the form page. The form page is also used as the parent page when the set parent page does not exist;
- `subroute` defines a route from the (initial) parent value. If one or more folders in the route do not exist they will be created;
- `slug_field` tells the plugin what field or fields to use as the new page's slug or folder name. Multiple form field names may be specified either using a comma separated string or as an array. When `slug_field` is missing the plugin tries to use the value of `title`;
- `overwrite_mode: true|false|edit` (default `false`) tells the plugin what to do when a page with the same name already exists. With `overwrite_mode: true` the existing page is overwritten. Any additional (media) files besides the page itself which are stored in the existing page folder are deleted as well. With `overwite_mode: false` the new page slug gets a sequential number attached at the end (for example "my-new-page-1" in case "my-new-page" exists).
Using `overwite_mode: edit` allows for the page being saved to it's existing folder respecting any already present uploaded files;
- `include_username: true|false` (default `false`) determines whether or not to include the username of a logged in frontend user in the new page frontmatter;
- `physical_template_name: true|false` (default `true`) does or does not cause the plugin to use the template name of the new page as that new page's filesystem filename. Defaults to "default" when no template is set in the 'pagefrontmatter' block. When set to `true` to avoid future confusion the frontmatter variable `template` is removed from the new page frontmatter.
#### A note on parent and subroute
Together the variables `parent` and `subroute` define the new page's destination. Or, in other words, together they set the path or route of the new page filesystem folder in the page structure.
The difference between parent and subroute worded in another way:
- Parent: works on a page level; when there is no page at the parent route, the form page is used as the parent;
- Subroute: works on a folder level; a subroute may consist of empty folders and if a folder in the subroute does not exist it gets created.
### 'pagefrontmatter' block variables
The content of the optional `pagefrontmatter` block will be included in the new page frontmatter.
## Form usage
The form page needs to use a [simple single form](https://learn.getgrav.org/forms/forms#create-a-simple-single-form).
Two examples are included at the end of this ReadMe file.
### Mandatory fields and values
#### Form Name
It is always a good thing to give each form a unique name, especially when multiple forms are used.
To pre fill form fields with default values the Form name must include the string "add_page". Valid names are for example `add_page.blogpost`, `add_page_profile`.
###Form Actions
**Custom Form processing** ( Important ! )
To let the plugin process the form after a Submit a custom process action must be set like so:
```
process:
-
add_page: true
```
**Redirect to the new page**
To show the new page to the user set the `redirect` action to the custom value `@self` or `@self-admin`.
When using `redirect: '@self'` the page will be shown as a regular web page, for example:
```
process:
-
add_page: true
-
redirect: '@self'
```
To open the new page in the Admin panel use `redirect: '@self-admin'`.
Note that this plugin does not handle the admin user authentication. If the Admin plugin is not installed or is inactive redirection occurs as if `@self` was used.
> Tip: using `@self-admin` is a very convenient way to learn how to use this plugin as it is easy to view and examine the source of the resulting new page including it's frontmatter in the Admin panel.
### Using a Markdown editor in textarea fields
When a `textarea` field is given the class `editor` it will use the [SimpleMDE Markdown Editor](https://simplemde.com).
## Value overrides
The variables which are defined and given a value in the `pageconfig` and `pagefrontmatter` blocks may be 'overridden' or in other words replaced by form input fields. In that respect these variables can be seen to hold a set of default values.
There is only one exception to the default variable override behaviour and that is the handling of `taxonomy` types. Extra taxonomy types and values (for example tags) which are entered via form fields are added to the new page taxonomy.
To override a default value by user input is simply a matter of including a form field by the same name in the page form.
For example in the example 2 - _create a new blog post_, the default title is set to "My new Blog post". The form contains a form field of type text with `name: title`. Thus the user is prompted to enter a title for the new page in the form but does not need to do so because filling in the title field is not mandatory. If the user enters a title that value is used as the title for the new page. If he or she does not, the default title "My new Blog post" will be used.
## Setting taxonomy categories and tags
The Add Blog Post example shows how to let the user add extra tags via the form.
Extra categories may be added in the same way.
## Handling extra taxonomy types
By default Grav 'knows' two taxonomy types, `category` and `tag`. Extra taxonomy types may be defined and added just like with any other variables you can include a form field. The new type is then added to the list of taxonomy types instead of replacing the existing types.
This can be done in the `pagefrontmatter` block. For example, to define a new taxonomy type named 'department':
```
pagefrontmatter:
taxonomy:
- department
```
And/or in the form:
```
form:
name: my_form
fields:
-
name: taxonomy
label: Taxonomy type
type: text
```
This is a feature which calls for a solid look-before-you-leap approach because of it's side effects. Using a new taxonomy type requires it to be included in the list of known taxonomy types. This list is in the site configuration file `site.yaml`.
By setting the plugin configuration option `auto_taxonomy_types: true` new types get automatically saved and can then be used in a collection.
The side effect and possibly downside is that every modification of the site configuration file causes Grav to rebuild the cache, so this may not be desirable with larger sites.
Use with caution!
## Examples
The least error prone way to test and play with the examples is to set up a fresh Grav site and using it's default theme Antimatter.
### Form page example 1: create a normal page
The goal of this example is to show how to let a user create a new page where uploaded images and files are saved along the page (in the same folder). After the user clicked Submit he or she will be shown the new page.
Suppose this minimal Grav website structure in `user/pages/`:
```
03.submit-assignment/
default.md
04.assignments/
cmpt363-e100/
default.md
drafts/
modular.md
reviewed/
modular.md
```
BTW both modular pages are not required but are mentioned as they could be used to display a collection of draft and reviewed assignments.
Then the 'Submit assignment for review' page (with slug `submit-assignment`) full content (both frontmatter and content) could look like:
```
---
routable: true
title: 'Submit assignment for review'
template: form
visible: true
pageconfig:
parent: /submitted-assignments/cmpt363-e100/drafts
pagefrontmatter:
visible: true
status: draft
template: default
course:
assignment: 'CMPT363 E100'
instructor:
name: 'Jane Doe'
form:
name: addpage-assignment-cmpt363-e100
fields:
-
name: name
label: Name
type: text
validate:
required: true
-
name: title
label: Title
type: text
validate:
required: true
-
name: content
label: 'Assignment text'
type: textarea
size: long
classes: editor
validate:
required: true
-
name: attachments
label: 'Attachment (PDF only)'
type: file
multiple: true
accept:
- application/pdf
validate:
required: false
-
name: honeypot
type: honeypot
buttons:
-
type: submit
value: Submit
process:
-
addpage: null
-
redirect: '@self'
---
Please write your assignment and attach any images and/or files.
```
Supposing the user has not changed the pre filled title field, has entered his name "Paul Walker", has entered a simple "q.e.d." as the assignment content and uploaded one PDF document, then the full new page will be:
```
---
visible: true
status: draft
course:
assignment: 'CMPT363 E100'
instructor:
name: 'Jane Doe'
name: 'Paul Walker'
title: 'CMPT363 E100'
attachments:
/Users/rwgc/devroot/repos/grav-test/htdocs/user/pages/assignments/cmpt363-e100/drafts/cmpt363-e100-1/scrum-guide-sept-2013.pdf
name: scrum-guide-sept-2013.pdf
type: application/pdf
size: 273 KB
path: /Users/rwgc/devroot/repos/grav-test/htdocs/user/pages/assignments/cmpt363-e100/drafts/cmpt363-e100-1/scrum-guide-sept-2013.pdf
---
q.e.d.
```
On the file system level the file structure will be:
```
01.home/
default.md
02.add-new-article/
default.md
03.assignments/
cmpt363-e100/
default.md
drafts/
cmpt363-e100-1/
default.md
scrum-guide-sept-2013.pdf
modular.md
reviewed/
modular.md
```
### Form page example 2: create a blog post
In this example the user can add a blog post. To ensure the new page will be treated as a blog post simply set the `template` variable to be used by the new page to `item`. BTW the active theme must include the corresponding template `item.html.twig`. This is why it is best to start with Grav's default theme Antimatter.
```
---
title: 'Add Blog Post'
template: form
pageconfig:
parent: '/blog'
include_username: true
overwrite_mode: true
pagefrontmatter:
template: item
title: My new Blog post
taxonomy:
category: blog
tag: [journal, guest]
form:
name: add_page.blogpost
fields:
-
name: author
label: 'Author'
type: text
-
name: title
label: 'Post Title'
type: text
-
name: taxonomy.tag
label: 'Tags (comma separated)'
type: text
-
name: content
label: 'Post Content'
type: textarea
size: long
classes: editor
-
name: images
label: 'Images to upload'
type: file
multiple: true
accept:
- 'image/*'
-
name: honeypot
type: honeypot
buttons:
-
type: submit
value: Submit
process:
-
add_page: true
-
redirect: '/blog'
---
## New Blog Post
Write your blog post:
```
After the form has been submitted the user is taken to the blog main page where the new post should show up.
## Issues
### Grav Form issue
The form on the form page is a standard Grav form. Please note that the Grav Form Plugin currently (latest test using version 4.0.1) has an issue which prevents the form to be submitted when a form field of type `file` is set to `required: true`(see issue [#106](https://github.com/getgrav/grav-plugin-form/issues/106)).
## Credits
- Team Grav and everyone who contributes to Grav;
- Wes Cossick for [SimpleMDE Markdown Editor](https://simplemde.com);
- All [contributors](https://github.com/bleutzinn/grav-plugin-add-page-by-form/graphs/contributors) who've helped me out on things.
File diff suppressed because it is too large Load Diff
@@ -1,9 +0,0 @@
enabled: true
date_display_format: 'd-m-Y H:i'
default_title: 'My New Page'
default_content: 'No content.'
overwrite_mode: false
include_username: false
auto_taxonomy_types: false
use_editor_class: true
physical_template_name: true
@@ -1,8 +0,0 @@
.editor-toolbar a.active, .editor-toolbar a:focus {
outline: none;
}
.CodeMirror, .CodeMirror-scroll {
min-height: 70px;
}
@@ -1,16 +0,0 @@
$(function () {
$(".editor").each(function(){
var simplemde = new SimpleMDE({
element: this,
forceSync: true,
hideIcons: ["side-by-side", "fullscreen"],
spellChecker: false,
toolbar: ["bold", "italic", "heading", "|",
"quote", "unordered-list", "ordered-list", "|",
"link", "table", "|",
"undo", "redo", "|",
"preview", "guide"
]
});
});
});
-109
View File
@@ -1,109 +0,0 @@
name: Add Page By Form
version: 3.3.0
description: Adds a page by means of a form
icon: plus-square-o
author:
name: Ron Wardenier
email: ron@wardenier.com
homepage: https://github.com/bleutzinn/grav-plugin-add-page-by-form
keywords: grav, plugin, page, form, frontmatter
bugs: https://github.com/bleutzinn/grav-plugin-add-page-by-form/issues
docs: https://github.com/bleutzinn/grav-plugin-add-page-by-form/blob/master/README.md
license: MIT
dependencies:
- { name: grav, version: '>=1.6.0' }
form:
validation: strict
fields:
enabled:
type: toggle
label: Plugin status
highlight: 1
default: 0
options:
1: Enabled
0: Disabled
validate:
type: bool
default_title:
type: text
size: large
label: Default Page Title
help: Will be used for the new page folder name when no other value is set
default_content:
type: text
size: large
label: Default Page Content
help: Will be used as the page content for the new page when no other value is set
include_username:
type: toggle
label: Include username
help: Include the logged in user username in the new page frontmatter
highlight: 0
default: 0
options:
1: Enabled
0: Disabled
validate:
type: bool
overwrite_mode:
type: select
label: Overwrite mode
help: Overwrite existing page
highlight: 0
default: false
options:
0: Disabled
1: Enabled
edit: Edit
auto_taxonomy_types:
type: toggle
label: Add new taxonomy types
help: Automatically add new taxonomy types to site configuration. May have performance impact on large sites.
highlight: 0
default: 0
options:
1: Enabled
0: Disabled
validate:
type: bool
use_editor_class:
type: toggle
label: Use the editor class
help: When set, adding class=editor to a textarea provides the simpleMDE editor on that area
highlight: 1
default: 1
options:
1: Enabled
0: Disabled
validate:
type: bool
date_display_format:
type: select
size: medium
classes: fancy
label: Date Format
default: 'd-m-Y H:i'
options:
'F jS Y': "February 1st 2014"
'l jS of F': "Saturday 1st of February"
'D, m M Y': "Sat, 01 Feb 2014"
'd-m-y': "01-02-14"
'd-m-Y': "01-02-2014"
'jS M Y': "1st Feb 2014"
'F Y': "Feb 2014"
'Y-m-d': "2014-02-01"
'd-m-Y H:i': "01-02-2014 09:30"
physical_template_name:
type: toggle
label: Physical template name
help: Use the template name in the filename of the new page
highlight: 1
default: 1
options:
1: Enabled
0: Disabled
validate:
type: bool
-35
View File
@@ -1,35 +0,0 @@
{
"name": "bleutzinn/grav-plugin-add-page-by-form",
"type": "grav-plugin",
"description": "Adds a page by means of a form",
"keywords": ["grav", "plugin", "page", "form", "frontmatter"],
"homepage": "https://github.com/bleutzinn/grav-plugin-add-page-by-form",
"license": "MIT",
"authors": [
{
"name": "Ron Wardenier",
"email": "bleutzinn@rwgc.nl",
"role": "Developer"
}
],
"support": {
"issues": "https://github.com/bleutzinn/grav-plugin-add-page-by-form/issues",
"docs": "https://github.com/bleutzinn/grav-plugin-add-page-by-form/blob/master/README.md"
},
"require": {
"php": ">=7.1.3"
},
"autoload": {
"psr-4": {
"Grav\\Plugin\\AddPageByFormPlugin\\": "classes/"
},
"classmap": [
"add-page-by-form.php"
]
},
"config": {
"platform": {
"php": "7.1.3"
}
}
}
-23
View File
@@ -1,23 +0,0 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "3d1383f8c010e20d78cbeeba97140c36",
"packages": [],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=7.1.3"
},
"platform-dev": [],
"platform-overrides": {
"php": "7.1.3"
},
"plugin-api-version": "2.2.0"
}
-7
View File
@@ -1,7 +0,0 @@
<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit0206cd2f5513395c6d5551a767d5c39f::getLoader();
-572
View File
@@ -1,572 +0,0 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var ?string */
private $vendorDir;
// PSR-4
/**
* @var array[]
* @psalm-var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array[]
* @psalm-var array<string, array<int, string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var array[]
* @psalm-var array<string, string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* @var array[]
* @psalm-var array<string, array<string, string[]>>
*/
private $prefixesPsr0 = array();
/**
* @var array[]
* @psalm-var array<string, string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var string[]
* @psalm-var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var bool[]
* @psalm-var array<string, bool>
*/
private $missingClasses = array();
/** @var ?string */
private $apcuPrefix;
/**
* @var self[]
*/
private static $registeredLoaders = array();
/**
* @param ?string $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
}
/**
* @return string[]
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array[]
* @psalm-return array<string, array<int, string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return array[]
* @psalm-return array<string, string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return array[]
* @psalm-return array<string, string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return string[] Array of classname => path
* @psalm-return array<string, string>
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param string[] $classMap Class to filename map
* @psalm-param array<string, string> $classMap
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param string[]|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
(array) $paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
(array) $paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = (array) $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
(array) $paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param string[]|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
(array) $paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
(array) $paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
(array) $paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param string[]|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param string[]|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders indexed by their corresponding vendor directories.
*
* @return self[]
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
* @private
*/
function includeFile($file)
{
include $file;
}
@@ -1,350 +0,0 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*/
class InstalledVersions
{
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}|array{}|null
*/
private static $installed;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || empty($installed['versions'][$packageName]['dev_requirement']);
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints($constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array<string, array{dev_requirement: bool, pretty_version?: string, version?: string, aliases?: string[], reference?: string, replaced?: string[], provided?: string[], install_path?: string, type?: string}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
if (self::$canGetVendors) {
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
$installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php';
if (null === self::$installed && strtr($vendorDir.'/composer', '\\', '/') === strtr(__DIR__, '\\', '/')) {
self::$installed = $installed[count($installed) - 1];
}
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = require __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
$installed[] = self::$installed;
return $installed;
}
}
-21
View File
@@ -1,21 +0,0 @@
Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -1,11 +0,0 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'Grav\\Plugin\\AddPageByFormPlugin' => $baseDir . '/add-page-by-form.php',
);
@@ -1,9 +0,0 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
);
@@ -1,10 +0,0 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'Grav\\Plugin\\AddPageByFormPlugin\\' => array($baseDir . '/classes'),
);
@@ -1,57 +0,0 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit0206cd2f5513395c6d5551a767d5c39f
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInit0206cd2f5513395c6d5551a767d5c39f', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
spl_autoload_unregister(array('ComposerAutoloaderInit0206cd2f5513395c6d5551a767d5c39f', 'loadClassLoader'));
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
if ($useStaticLoader) {
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit0206cd2f5513395c6d5551a767d5c39f::getInitializer($loader));
} else {
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
$loader->set($namespace, $path);
}
$map = require __DIR__ . '/autoload_psr4.php';
foreach ($map as $namespace => $path) {
$loader->setPsr4($namespace, $path);
}
$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
$loader->addClassMap($classMap);
}
}
$loader->register(true);
return $loader;
}
}
@@ -1,37 +0,0 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit0206cd2f5513395c6d5551a767d5c39f
{
public static $prefixLengthsPsr4 = array (
'G' =>
array (
'Grav\\Plugin\\AddPageByFormPlugin\\' => 32,
),
);
public static $prefixDirsPsr4 = array (
'Grav\\Plugin\\AddPageByFormPlugin\\' =>
array (
0 => __DIR__ . '/../..' . '/classes',
),
);
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'Grav\\Plugin\\AddPageByFormPlugin' => __DIR__ . '/../..' . '/add-page-by-form.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit0206cd2f5513395c6d5551a767d5c39f::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit0206cd2f5513395c6d5551a767d5c39f::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInit0206cd2f5513395c6d5551a767d5c39f::$classMap;
}, null, ClassLoader::class);
}
}
@@ -1,5 +0,0 @@
{
"packages": [],
"dev": false,
"dev-package-names": []
}
-23
View File
@@ -1,23 +0,0 @@
<?php return array(
'root' => array(
'pretty_version' => '1.0.0+no-version-set',
'version' => '1.0.0.0',
'type' => 'grav-plugin',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'reference' => NULL,
'name' => 'bleutzinn/grav-plugin-add-page-by-form',
'dev' => false,
),
'versions' => array(
'bleutzinn/grav-plugin-add-page-by-form' => array(
'pretty_version' => '1.0.0+no-version-set',
'version' => '1.0.0.0',
'type' => 'grav-plugin',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'reference' => NULL,
'dev_requirement' => false,
),
),
);
@@ -1,26 +0,0 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 70103)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 7.1.3". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
} elseif (!headers_sent()) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
trigger_error(
'Composer detected issues in your platform: ' . implode(' ', $issues),
E_USER_ERROR
);
}
-8
View File
@@ -1,8 +0,0 @@
# Linguist Normalizer
*.yaml linguistic-language=PHP
*.twig linguistic-language=PHP
**/gulpfile.babel.js linguist-vendored
**/webpack.conf.js linguist-vendored
**/js/*.js linguist-vendored
**/js/*.json linguist-vendored
**/css-compiled/*.css linguist-vendored
-8
View File
@@ -1,8 +0,0 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: grav
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
custom: # Replace with a single custom sponsorship URL
-6
View File
@@ -1,6 +0,0 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
File diff suppressed because it is too large Load Diff
-1
View File
@@ -1 +0,0 @@
Please read the <a href="https://github.com/getgrav/grav/blob/develop/CONTRIBUTING.md" target="_blank">Contributing Guidelines of the Grav Project</a>
-21
View File
@@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2017 Grav
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-152
View File
@@ -1,152 +0,0 @@
# Grav Standard Administration Panel Plugin
This **admin plugin** for [Grav](https://github.com/getgrav/grav) is an HTML user interface that provides a convenient way to configure Grav and easily create and modify pages. This will remain a totally optional plugin, and is not in any way required or needed to use Grav effectively. In fact, the admin provides an intentionally limited view to ensure it remains easy to use and not overwhelming. I'm sure power users will still prefer to work with the configuration files directly.
![](assets/admin-dashboard.png)
# Features
* User login with automatic password encryption
* Forgot password functionality
* Logged-in-user management
* One click Grav core updates
* Dashboard with maintenance status, site activity and latest page updates
* Notifications system for latest news, blogs, and announcements
* Ajax-powered backup capability
* Ajax-powered clear-cache capability
* System configuration management
* Site configuration management
* Normal and Expert modes which allow editing via forms or YAML
* Page listing with filtering and search
* Page creation, editing, moving, copying, and deleting
* Powerful syntax highlighting code editor with instant Grav-powered preview
* Editor features, hot keys, toolbar, and distraction-free fullscreen mode
* Drag-n-drop upload of page media files including drag-n-drop placement in the editor
* One click theme and plugin updates
* Plugin manager that allows listing and configuration of installed plugins
* Theme manager that allows listing and configuration of installed themes
* GPM-powered installation of new plugins and themes
# Support
#### Support
We have tested internally, but we hope to use this public beta phase to identify, isolate, and fix issues related to the plugin to ensure it is as solid and reliable as possible.
For **live chatting**, please use the dedicated [Discord Chat Room](https://getgrav.org/discord) for discussions directly related to Grav.
For **bugs, features, improvements**, please ensure you [create issues in the admin plugin GitHub repository](https://github.com/getgrav/grav-plugin-admin).
# Installation
First ensure you are running the latest **Grav 1.6.7 or later**. This is required for the admin plugin to run properly (`-f` forces a refresh of the GPM index).
```
$ bin/gpm selfupgrade -f
```
The admin plugin actually requires the help of 3 other plugins, so to get the admin plugin to work you first need to install **admin**, **login**, **forms**, and **email** plugins. These are available via GPM, and because the plugin has dependencies you just need to proceed and install the admin plugin, and agree when prompted to install the others:
```
$ bin/gpm install admin
```
### Manual Installation
Manual installation is not the recommended method of installation, however, it is still possible to install the admin plugin manually. Basically, you need to download each of the following plugins individually:
* [admin](https://github.com/getgrav/grav-plugin-admin/archive/develop.zip)
* [login](https://github.com/getgrav/grav-plugin-login/archive/develop.zip)
* [form](https://github.com/getgrav/grav-plugin-form/archive/develop.zip)
* [email](https://github.com/getgrav/grav-plugin-email/archive/develop.zip)
Extract each archive file into your `user/plugins` folder, then ensure the folders are renamed to just `admin/`, `login/`, `form/`, and `email/`. Then proceed with the **Usage instructions below**.
# Usage
### Create User with CLI
After this you need to create a user account with admin privileges:
```
$ bin/plugin login new-user
```
### Create User Manually
Alternatively, you can create a user account manually, in a file called `user/accounts/admin.yaml`. This **filename** is actually the **username** that you will use to login. The contents will contain the other information for the user.
```
password: 'password'
email: 'youremail@mail.com'
fullname: 'Johnny Appleseed'
title: 'Site Administrator'
access:
admin:
login: true
super: true
```
Of course you should edit your `email`, `password`, `fullname`, and `title` to suit your needs.
> You can use any password when you manually put it in this `.yaml` file. However, when you change your password in the admin, it must contain at least one number and one uppercase and lowercase letter, and at least 8 or more characters.
# Accessing the Admin
By default, you can access the admin by pointing your browser to `http://yoursite.com/admin`. You can simply log in with the `username` and `password` set in the YAML file you configured earlier.
> After logging in, your **plaintext password** will be removed and replaced by an **encrypted** one.
# Standard Free & Paid Pro Versions
If you have been following the [blog](https://getgrav.org/blog), [Twitter](https://twitter.com/getgrav), [Discord chat](https://getgrav.org/discord), etc., you probably already know now that our intention is to provide two versions of this plugin.
The **standard free version**, is very powerful, and has more functionality than most commercial flat-file CMS systems.
We also intend to release in the near future a more feature-rich **pro version** that will include enhanced functionality, as well as some additional nice-to-have capabilities. This pro version will be a **paid** plugin the price of which is not yet 100% finalized.
# Admin Events
## General events
- onAdminRegisterPermissions - (admin)
- onAdminThemeInitialized
- onAdminPage - (page)
- onAdminMenu
- onAdminTwigTemplatePaths - (paths)
## Page specific events
- onAdminDashboard
- onAdminTools - (tools)
- onAdminLogFiles - (logs)
- onAdminGenerateReports - (reports)
## Tasks
- onAdminControllerInit - (controller)
- onAdminTaskExecute - (controller, method)
## Editing
- onAdminData
- onAdminSave - (object)
- onAdminAfterSave - (object)
## Pages
- onAdminPageTypes - (types)
- onAdminModularPageTypes
- onAdminSave - (page)
- onAdminAfterSaveAs - (path)
- onAdminAfterSave - (page)
- onAdminAfterDelete - (page)
- onAdminAfterAddMedia - (page)
- onAdminAfterDelMedia - (page)
- onAdminCreatePageFrontmatter - (header, data)
# Running Tests
First install the dev dependencies by running `composer update` from the Grav root.
Then `composer test` will run the Unit Tests, which should be always executed successfully on any site.
-6
View File
@@ -1,6 +0,0 @@
# Upgrading to Admin 1.10
Twig:
* **Admin link**: When linking to another admin page, use `{{ admin_route('/config/site') }}` instead of any other method, such as `{{ base_url_relative }}/config/site` (fixes multi-language issues)
File diff suppressed because it is too large Load Diff
-85
View File
@@ -1,85 +0,0 @@
enabled: false
route: '/admin'
cache_enabled: true
theme: grav
logo_text: ''
body_classes: ''
content_padding: true
twofa_enabled: true
sidebar:
activate: tab
hover_delay: 100
size: auto
dashboard:
days_of_stats: 7
widgets_display:
dashboard-maintenance: true
dashboard-statistics: true
dashboard-notifications: true
dashboard-feed: true
dashboard-pages: true
pages:
show_parents: both
show_modular: true
session:
timeout: 1800
keep_alive: true
edit_mode: normal
frontend_preview_target: inline
show_github_msg: true
admin_icons: line-awesome
enable_auto_updates_check: true
notifications:
feed: true
dashboard: true
plugins: true
themes: true
popularity:
enabled: true
ignore: ['/test*','/modular']
history:
daily: 30
monthly: 12
visitors: 20
whitelabel:
quicktray_recompile: false
codemirror_theme: paper
codemirror_fontsize: md
codemirror_md_font: sans
logo_custom:
logo_login:
color_scheme:
accents:
primary-accent: button
secondary-accent: notice
tertiary-accent: critical
colors:
logo-bg: '#323640'
logo-link: '#FFFFFF'
nav-bg: '#3D424E'
nav-text: '#B7B9BD'
nav-link: '#ffffff'
nav-selected-bg: '#323640'
nav-selected-link: '#ffffff'
nav-hover-bg: '#434753'
nav-hover-link: '#ffffff'
toolbar-bg: '#ffffff'
toolbar-text: '#3D424E'
page-bg: '#F6F6F6'
page-text: '#6f7b8a'
page-link: '#0090D9'
content-bg: '#ffffff'
content-text: '#6f7b8a'
content-link: '#0090D9'
content-link2: '#da4b46'
content-header: '#414147'
content-tabs-bg: '#e6e6e6'
content-tabs-text: '#808080'
button-bg: '#0090D9'
button-text: '#ffffff'
notice-bg: '#06A599'
notice-text: '#ffffff'
update-bg: '#77559D'
update-text: '#ffffff'
critical-bg: '#F45857'
critical-text: '#ffffff'
Binary file not shown.

Before

Width:  |  Height:  |  Size: 236 KiB

-792
View File
@@ -1,792 +0,0 @@
name: Admin Panel
slug: admin
type: plugin
testing: true
version: 1.11.0-beta.5
description: Adds an advanced administration panel to manage your site
icon: empire
author:
name: Team Grav
email: devs@getgrav.org
url: https://getgrav.org
homepage: https://github.com/getgrav/grav-plugin-admin
keywords: admin, plugin, manager, panel
bugs: https://github.com/getgrav/grav-plugin-admin/issues
docs: https://github.com/getgrav/grav-plugin-admin/blob/develop/README.md
license: MIT
dependencies:
- { name: grav, version: '>=1.8.0-beta.1' }
- { name: form, version: '>=6.0.1' }
- { name: login, version: '>=3.7.8' }
- { name: email, version: '>=3.1.6' }
- { name: flex-objects, version: '>=1.4.0-beta.1' }
form:
validation: loose
fields:
admin_tabs:
type: tabs
fields:
config_tab:
type: tab
title: PLUGIN_ADMIN.CONFIGURATION
fields:
Basics:
type: section
title: PLUGIN_ADMIN.BASICS
underline: false
enabled:
type: hidden
label: PLUGIN_ADMIN.PLUGIN_STATUS
highlight: 1
default: 0
options:
1: PLUGIN_ADMIN.ENABLED
0: PLUGIN_ADMIN.DISABLED
validate:
type: bool
cache_enabled:
type: toggle
label: PLUGIN_ADMIN.ADMIN_CACHING
help: PLUGIN_ADMIN.ADMIN_CACHING_HELP
highlight: 1
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
twofa_enabled:
type: toggle
label: PLUGIN_LOGIN.2FA_TITLE
help: PLUGIN_LOGIN.2FA_ENABLED_HELP
default: 1
highlight: 1
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
route:
type: text
label: PLUGIN_ADMIN.ADMIN_PATH
size: medium
placeholder: ADMIN_PATH_PLACEHOLDER
help: ADMIN_PATH_HELP
logo_text:
type: text
label: PLUGIN_ADMIN.LOGO_TEXT
size: medium
placeholder: "Grav"
help: PLUGIN_ADMIN.LOGO_TEXT_HELP
content_padding:
type: toggle
label: PLUGIN_ADMIN.CONTENT_PADDING
help: PLUGIN_ADMIN.CONTENT_PADDING_HELP
highlight: 1
options:
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
body_classes:
type: text
label: PLUGIN_ADMIN.BODY_CLASSES
size: medium
help: PLUGIN_ADMIN.BODY_CLASSES_HELP
sidebar.activate:
type: select
label: PLUGIN_ADMIN.SIDEBAR_ACTIVATION
help: PLUGIN_ADMIN.SIDEBAR_ACTIVATION_HELP
size: small
default: tab
options:
tab: PLUGIN_ADMIN.SIDEBAR_ACTIVATION_TAB
hover: PLUGIN_ADMIN.SIDEBAR_ACTIVATION_HOVER
sidebar.hover_delay:
type: text
size: x-small
append: PLUGIN_ADMIN.SIDEBAR_HOVER_DELAY_APPEND
label: PLUGIN_ADMIN.SIDEBAR_HOVER_DELAY
default: 500
validate:
type: number
min: 1
sidebar.size:
type: select
label: PLUGIN_ADMIN.SIDEBAR_SIZE
help: PLUGIN_ADMIN.SIDEBAR_SIZE_HELP
size: medium
default: auto
options:
auto: PLUGIN_ADMIN.SIDEBAR_SIZE_AUTO
small: PLUGIN_ADMIN.SIDEBAR_SIZE_SMALL
theme:
type: hidden
label: PLUGIN_ADMIN.THEME
default: grav
edit_mode:
type: select
label: PLUGIN_ADMIN.EDIT_MODE
size: small
default: normal
options:
normal: PLUGIN_ADMIN.NORMAL
expert: PLUGIN_ADMIN.EXPERT
help: PLUGIN_ADMIN.EDIT_MODE_HELP
frontend_preview_target:
type: select
label: PLUGIN_ADMIN.FRONTEND_PREVIEW_TARGET
size: medium
default: inline
options:
inline: PLUGIN_ADMIN.FRONTEND_PREVIEW_TARGET_INLINE
_blank: PLUGIN_ADMIN.FRONTEND_PREVIEW_TARGET_NEW
_self: PLUGIN_ADMIN.FRONTEND_PREVIEW_TARGET_CURRENT
pages.show_parents:
type: select
size: medium
label: PLUGIN_ADMIN.PARENT_DROPDOWN
highlight: 1
options:
both: PLUGIN_ADMIN.PARENT_DROPDOWN_BOTH
folder: PLUGIN_ADMIN.PARENT_DROPDOWN_FOLDER
fullpath: PLUGIN_ADMIN.PARENT_DROPDOWN_FULLPATH
pages.parents_levels:
type: text
label: PLUGIN_ADMIN.PARENTS_LEVELS
size: small
help: PLUGIN_ADMIN.PARENTS_LEVELS_HELP
pages.show_modular:
type: toggle
label: PLUGIN_ADMIN.MODULAR_PARENTS
highlight: 1
default: 1
options:
1: PLUGIN_ADMIN.ENABLED
0: PLUGIN_ADMIN.DISABLED
validate:
type: bool
help: PLUGIN_ADMIN.MODULAR_PARENTS_HELP
show_beta_msg:
type: hidden
show_github_msg:
type: toggle
label: PLUGIN_ADMIN.SHOW_GITHUB_LINK
highlight: 1
default: 1
options:
1: PLUGIN_ADMIN.ENABLED
0: PLUGIN_ADMIN.DISABLED
validate:
type: bool
help: PLUGIN_ADMIN.SHOW_GITHUB_LINK_HELP
enable_auto_updates_check:
type: toggle
label: PLUGIN_ADMIN.AUTO_UPDATES
highlight: 1
default: 1
options:
1: PLUGIN_ADMIN.ENABLED
0: PLUGIN_ADMIN.DISABLED
validate:
type: bool
help: PLUGIN_ADMIN.AUTO_UPDATES_HELP
session.timeout:
type: text
size: small
label: PLUGIN_ADMIN.TIMEOUT
append: GRAV.NICETIME.SECOND_PLURAL
help: PLUGIN_ADMIN.TIMEOUT_HELP
validate:
type: number
min: 1
session.keep_alive:
type: toggle
label: Keep Alive Ping
help: "Periodically pings to keep your admin session alive. Turn OFF to allow the session to expire while idle (useful for testing timeouts)."
highlight: 1
default: 1
options:
1: PLUGIN_ADMIN.ENABLED
0: PLUGIN_ADMIN.DISABLED
validate:
type: bool
hide_page_types:
type: select
size: large
label: PLUGIN_ADMIN.HIDE_PAGE_TYPES
classes: fancy
multiple: true
array: true
selectize:
create: true
data-options@: ['\Grav\Plugin\AdminPlugin::pagesTypes', true]
hide_modular_page_types:
type: select
size: large
label: PLUGIN_ADMIN.HIDE_MODULAR_PAGE_TYPES
classes: fancy
multiple: true
array: true
selectize:
create: true
data-options@: ['\Grav\Plugin\AdminPlugin::pagesModularTypes', true]
Dashboard:
type: section
title: PLUGIN_ADMIN.DASHBOARD
underline: true
widgets_display:
type: widgets
label: PLUGIN_ADMIN.WIDGETS_DISPLAY
validate:
type: array
Notifications:
type: section
title: PLUGIN_ADMIN.NOTIFICATIONS
underline: true
notifications.feed:
type: toggle
label: PLUGIN_ADMIN.FEED_NOTIFICATIONS
highlight: 1
default: 1
options:
1: PLUGIN_ADMIN.ENABLED
0: PLUGIN_ADMIN.DISABLED
validate:
type: bool
help: PLUGIN_ADMIN.FEED_NOTIFICATIONS_HELP
notifications.dashboard:
type: toggle
label: PLUGIN_ADMIN.DASHBOARD_NOTIFICATIONS
highlight: 1
default: 1
options:
1: PLUGIN_ADMIN.ENABLED
0: PLUGIN_ADMIN.DISABLED
validate:
type: bool
help: PLUGIN_ADMIN.DASHBOARD_NOTIFICATIONS_HELP
notifications.plugins:
type: toggle
label: PLUGIN_ADMIN.PLUGINS_NOTIFICATIONS
highlight: 1
default: 1
options:
1: PLUGIN_ADMIN.ENABLED
0: PLUGIN_ADMIN.DISABLED
validate:
type: bool
help: PLUGIN_ADMIN.PLUGINS_NOTIFICATIONS_HELP
notifications.themes:
type: toggle
label: PLUGIN_ADMIN.THEMES_NOTIFICATIONS
highlight: 1
default: 1
options:
1: PLUGIN_ADMIN.ENABLED
0: PLUGIN_ADMIN.DISABLED
validate:
type: bool
help: PLUGIN_ADMIN.THEMES_NOTIFICATIONS_HELP
customization_tab:
type: tab
title: PLUGIN_ADMIN.CUSTOMIZATION
fields:
whitelabel.logos:
type: section
underline: true
title: PLUGIN_ADMIN.LOGOS
whitelabel.logo_login:
type: file
label: PLUGIN_ADMIN.LOGIN_SCREEN_CUSTOM_LOGO_LABEL
destination: 'user://assets'
accept:
- image/*
whitelabel.logo_custom:
type: file
label: PLUGIN_ADMIN.TOP_LEFT_CUSTOM_LOGO_LABEL
destination: 'user://assets'
accept:
- image/*
codemirror_section:
type: section
underline: true
title: PLUGIN_ADMIN.CODEMIRROR
whitelabel.codemirror_theme:
type: select
label: PLUGIN_ADMIN.CODEMIRROR_THEME
default: paper
markdown: true
data-options@: '\Grav\Plugin\AdminPlugin::themeOptions'
description: PLUGIN_ADMIN.CODEMIRROR_THEME_DESC
whitelabel.codemirror_fontsize:
type: select
label: PLUGIN_ADMIN.CODEMIRROR_FONTSIZE
default: md
options:
sm: PLUGIN_ADMIN.CODEMIRROR_FONTSIZE_SM
md: PLUGIN_ADMIN.CODEMIRROR_FONTSIZE_MD
lg: PLUGIN_ADMIN.CODEMIRROR_FONTSIZE_LG
whitelabel.codemirror_md_font:
type: select
label: PLUGIN_ADMIN.CODEMIRROR_MD_FONT
default: sans
options:
sans: PLUGIN_ADMIN.CODEMIRROR_MD_FONT_SANS
mono: PLUGIN_ADMIN.CODEMIRROR_MD_FONT_MONO
customization_section:
type: section
underline: true
title: PLUGIN_ADMIN.CUSTOMIZATION
whitelabel.quicktray_recompile:
type: toggle
label: PLUGIN_ADMIN.QUICKTRAY_RECOMPILE
help: PLUGIN_ADMIN.QUICKTRAY_RECOMPILE_HELP
highlight: 0
default: 0
options:
1: PLUGIN_ADMIN.ENABLED
0: PLUGIN_ADMIN.DISABLED
validate:
type: bool
whitelabel.color_scheme.name:
type: text
label: PLUGIN_ADMIN.COLOR_SCHEME_NAME
help: PLUGIN_ADMIN.COLOR_SCHEME_NAME_HELP
placeholder: PLUGIN_ADMIN.COLOR_SCHEME_NAME_PLACEHOLDER
themes-preview:
type: themepreview
ignore: true;
label: PLUGIN_ADMIN.PRESETS
style: vertical
colorschemes:
type: colorscheme
label: PLUGIN_ADMIN.COLOR_SCHEME_LABEL
style: vertical
help: PLUGIN_ADMIN.COLOR_SCHEME_HELP
fields:
whitelabel.color_scheme.colors.logo-bg:
type: colorscheme.color
default: '#1e333e'
help: PLUGIN_ADMIN.LOGO_BG_HELP
whitelabel.color_scheme.colors.logo-link:
type: colorscheme.color
default: '#ffffff'
help: PLUGIN_ADMIN.LOGO_LINK_HELP
whitelabel.color_scheme.colors.nav-bg:
type: colorscheme.color
default: '#253a47'
help: PLUGIN_ADMIN.NAV_BG_HELP
whitelabel.color_scheme.colors.nav-text:
type: colorscheme.color
default: '#afc7d5'
help: PLUGIN_ADMIN.NAV_TEXT_HELP
whitelabel.color_scheme.colors.nav-link:
type: colorscheme.color
default: '#d1dee7'
help: PLUGIN_ADMIN.NAV_LINK_HELP
whitelabel.color_scheme.colors.nav-selected-bg:
type: colorscheme.color
default: '#2d4d5b'
help: PLUGIN_ADMIN.NAV_SELECTED_BG_HELP
whitelabel.color_scheme.colors.nav-selected-link:
type: colorscheme.color
default: '#ffffff'
help: PLUGIN_ADMIN.NAV_SELECTED_LINK_HELP
whitelabel.color_scheme.colors.nav-hover-bg:
type: colorscheme.color
default: '#1e333e'
help: PLUGIN_ADMIN.NAV_HOVER_BG_HELP
whitelabel.color_scheme.colors.nav-hover-link:
type: colorscheme.color
default: '#ffffff'
help: PLUGIN_ADMIN.NAV_HOVER_LINK_HELP
whitelabel.color_scheme.colors.toolbar-bg:
type: colorscheme.color
default: '#349886'
help: PLUGIN_ADMIN.TOOLBAR_BG_HELP
whitelabel.color_scheme.colors.toolbar-text:
type: colorscheme.color
default: '#ffffff'
help: PLUGIN_ADMIN.TOOLBAR_TEXT_HELP
whitelabel.color_scheme.colors.page-bg:
type: colorscheme.color
default: '#314d5b'
help: PLUGIN_ADMIN.PAGE_BG_HELP
whitelabel.color_scheme.colors.page-text:
type: colorscheme.color
default: '#81a5b5'
help: PLUGIN_ADMIN.PAGE_TEXT_HELP
whitelabel.color_scheme.colors.page-link:
type: colorscheme.color
default: '#aad9ed'
help: PLUGIN_ADMIN.PAGE_LINK_HELP
whitelabel.color_scheme.colors.content-bg:
type: colorscheme.color
default: '#eeeeee'
help: PLUGIN_ADMIN.CONTENT_BG_HELP
whitelabel.color_scheme.colors.content-text:
type: colorscheme.color
default: '#737c81'
help: PLUGIN_ADMIN.CONTENT_TEXT_HELP
whitelabel.color_scheme.colors.content-link:
type: colorscheme.color
default: '#0082ba'
help: PLUGIN_ADMIN.CONTENT_LINK_HELP
whitelabel.color_scheme.colors.content-link2:
type: colorscheme.color
default: '#da4b46'
help: PLUGIN_ADMIN.CONTENT_LINK2_HELP
whitelabel.color_scheme.colors.content-header:
type: colorscheme.color
default: '#314d5b'
help: PLUGIN_ADMIN.CONTENT_HEADER_HELP
whitelabel.color_scheme.colors.content-tabs-bg:
type: colorscheme.color
default: '#223a47'
help: PLUGIN_ADMIN.CONTENT_TABS_BG_HELP
whitelabel.color_scheme.colors.content-tabs-text:
type: colorscheme.color
default: '#d1dee7'
help: PLUGIN_ADMIN.CONTENT_TABS_TEXT_HELP
whitelabel.color_scheme.colors.content-highlight:
type: colorscheme.color
default: '#ffffd7'
help: PLUGIN_ADMIN.CONTENT_HIGHLIGHT_HELP
whitelabel.color_scheme.colors.button-bg:
type: colorscheme.color
default: '#41bea8'
help: PLUGIN_ADMIN.BUTTON_BG_HELP
whitelabel.color_scheme.colors.button-text:
type: colorscheme.color
default: '#ffffff'
help: PLUGIN_ADMIN.BUTTON_TEXT_HELP
whitelabel.color_scheme.colors.notice-bg:
type: colorscheme.color
default: '#00a6cf'
help: PLUGIN_ADMIN.NOTICE_BG_HELP
whitelabel.color_scheme.colors.notice-text:
type: colorscheme.color
default: '#ffffff'
help: PLUGIN_ADMIN.NOTICE_TEXT_HELP
whitelabel.color_scheme.colors.update-bg:
type: colorscheme.color
default: '#8f5aad'
help: PLUGIN_ADMIN.UPDATES_BG_HELP
whitelabel.color_scheme.colors.update-text:
type: colorscheme.color
default: '#ffffff'
help: PLUGIN_ADMIN.UPDATES_TEXT_HELP
whitelabel.color_scheme.colors.critical-bg:
type: colorscheme.color
default: '#da4b46'
help: PLUGIN_ADMIN.CRITICAL_BG_HELP
whitelabel.color_scheme.colors.critical-text:
type: colorscheme.color
default: '#ffffff'
help: PLUGIN_ADMIN.CRITICAL_TEXT_HELP
whitelabel.color_scheme.accents.primary-accent:
type: select
size: medium
classes: fancy
label: PLUGIN_ADMIN.PRIMARY_ACCENT_LABEL
help: PLUGIN_ADMIN.PRIMARY_ACCENT_HELP
options:
button: PLUGIN_ADMIN.BUTTON_COLORS
content: PLUGIN_ADMIN.CONTENT_COLORS
tabs: PLUGIN_ADMIN.TABS_COLORS
critical: PLUGIN_ADMIN.CRITICAL_COLORS
logo: PLUGIN_ADMIN.LOGO_COLORS
nav: PLUGIN_ADMIN.NAV_COLORS
notice: PLUGIN_ADMIN.NOTICE_COLORS
page: PLUGIN_ADMIN.PAGE_COLORS
toolbar: PLUGIN_ADMIN.TOOLBAR_COLORS
update: PLUGIN_ADMIN.UPDATE_COLORS
whitelabel.color_scheme.accents.secondary-accent:
type: select
size: medium
classes: fancy
label: PLUGIN_ADMIN.SECONDARY_ACCENT_LABEL
help: PLUGIN_ADMIN.SECONDARY_ACCENT_HELP
options:
button: PLUGIN_ADMIN.BUTTON_COLORS
content: PLUGIN_ADMIN.CONTENT_COLORS
tabs: PLUGIN_ADMIN.TABS_COLORS
critical: PLUGIN_ADMIN.CRITICAL_COLORS
logo: PLUGIN_ADMIN.LOGO_COLORS
nav: PLUGIN_ADMIN.NAV_COLORS
notice: PLUGIN_ADMIN.NOTICE_COLORS
page: PLUGIN_ADMIN.PAGE_COLORS
toolbar: PLUGIN_ADMIN.TOOLBAR_COLORS
update: PLUGIN_ADMIN.UPDATE_COLORS
whitelabel.color_scheme.accents.tertiary-accent:
type: select
size: medium
classes: fancy
label: PLUGIN_ADMIN.TERTIARY_ACCENT_LABEL
help: PLUGIN_ADMIN.TERTIARY_ACCENT_HELP
options:
button: PLUGIN_ADMIN.BUTTON_COLORS
content: PLUGIN_ADMIN.CONTENT_COLORS
tabs: PLUGIN_ADMIN.TABS_COLORS
critical: PLUGIN_ADMIN.CRITICAL_COLORS
logo: PLUGIN_ADMIN.LOGO_COLORS
nav: PLUGIN_ADMIN.NAV_COLORS
notice: PLUGIN_ADMIN.NOTICE_COLORS
page: PLUGIN_ADMIN.PAGE_COLORS
toolbar: PLUGIN_ADMIN.TOOLBAR_COLORS
update: PLUGIN_ADMIN.UPDATE_COLORS
whitelabel.custom_footer:
type: textarea
rows: 2
label: PLUGIN_ADMIN.CUSTOM_FOOTER
help: PLUGIN_ADMIN.CUSTOM_FOOTER_HELP
placeholder: PLUGIN_ADMIN.CUSTOM_FOOTER_PLACEHOLDER
whitelabel.custom_css:
label: PLUGIN_ADMIN.CUSTOM_CSS_LABEL
placeholder: PLUGIN_ADMIN.CUSTOM_CSS_PLACEHOLDER
help: PLUGIN_ADMIN.CUSTOM_CSS_HELP
type: editor
codemirror:
mode: 'css'
indentUnit: 2
indentWithTabs: true
lineNumbers: true
styleActiveLine: true
whitelabel.custom_presets:
label: PLUGIN_ADMIN.CUSTOM_PRESETS
help: PLUGIN_ADMIN.CUSTOM_PRESETS_HELP
placeholder: PLUGIN_ADMIN.CUSTOM_PRESETS_PLACEHOLDER
type: editor
codemirror:
mode: 'yaml'
indentUnit: 2
indentWithTabs: false
lineNumbers: true
styleActiveLine: true
gutters: ['CodeMirror-lint-markers']
lint: true
extras_tab:
type: tab
title: PLUGIN_ADMIN.EXTRAS
fields:
Popularity:
type: section
title: PLUGIN_ADMIN.POPULARITY
underline: true
popularity.enabled:
type: toggle
label: PLUGIN_ADMIN.VISITOR_TRACKING
highlight: 1
default: 1
options:
1: PLUGIN_ADMIN.ENABLED
0: PLUGIN_ADMIN.DISABLED
validate:
type: bool
help: PLUGIN_ADMIN.VISITOR_TRACKING_HELP
dashboard.days_of_stats:
type: text
label: PLUGIN_ADMIN.DAYS_OF_STATS
append: days
size: x-small
default: 7
help: PLUGIN_ADMIN.DAYS_OF_STATS_HELP
validate:
type: int
popularity.ignore:
type: array
label: PLUGIN_ADMIN.IGNORE_URLS
size: large
help: PLUGIN_ADMIN.IGNORE_URLS_HELP
default: ['/test*','/modular']
value_only: true
placeholder_value: /ignore-this-route
popularity.history.daily:
type: hidden
label: PLUGIN_ADMIN.DAILY_HISTORY
default: 30
popularity.history.monthly:
type: hidden
label: PLUGIN_ADMIN.MONTHLY_HISTORY
default: 12
popularity.history.visitors:
type: hidden
label: PLUGIN_ADMIN.VISITORS_HISTORY
default: 20
MediaResize:
type: section
title: PLUGIN_ADMIN.MEDIA_RESIZE
underline: true
MediaResizeNote:
type: spacer
text: PLUGIN_ADMIN.PAGEMEDIA_RESIZER
markdown: true
pagemedia.resize_width:
type: number
size: x-small
append: PLUGIN_ADMIN.PIXELS
label: PLUGIN_ADMIN.RESIZE_WIDTH
default: 0
validate:
type: number
help: PLUGIN_ADMIN.RESIZE_WIDTH_HELP
pagemedia.resize_height:
type: number
size: x-small
append: PLUGIN_ADMIN.PIXELS
label: PLUGIN_ADMIN.RESIZE_HEIGHT
default: 0
validate:
type: number
help: PLUGIN_ADMIN.RESIZE_HEIGHT_HELP
pagemedia.res_min_width:
type: number
size: x-small
append: PLUGIN_ADMIN.PIXELS
label: PLUGIN_ADMIN.RES_MIN_WIDTH
default: 0
validate:
type: number
help: PLUGIN_ADMIN.RES_MIN_WIDTH_HELP
pagemedia.res_min_height:
type: number
size: x-small
append: PLUGIN_ADMIN.PIXELS
label: PLUGIN_ADMIN.RES_MIN_HEIGHT
default: 0
validate:
type: number
help: PLUGIN_ADMIN.RES_MIN_HEIGHT_HELP
pagemedia.res_max_width:
type: number
size: x-small
append: PLUGIN_ADMIN.PIXELS
label: PLUGIN_ADMIN.RES_MAX_WIDTH
default: 0
validate:
type: number
help: PLUGIN_ADMIN.RES_MAX_WIDTH_HELP
pagemedia.res_max_height:
type: number
size: x-small
append: PLUGIN_ADMIN.PIXELS
label: PLUGIN_ADMIN.RES_MAX_HEIGHT
default: 0
validate:
type: number
help: PLUGIN_ADMIN.RES_MAX_HEIGHT_HELP
pagemedia.resize_quality:
type: number
size: x-small
append: 0...1
label: PLUGIN_ADMIN.RESIZE_QUALITY
default: 0.8
validate:
type: number
step: 0.01
help: PLUGIN_ADMIN.RESIZE_QUALITY_HELP
@@ -1,43 +0,0 @@
rules:
slug:
pattern: '[a-zA-Zа-яA-Я0-9_\-]+'
min: 1
max: 200
form:
validation: loose
fields:
section:
type: section
title: PLUGIN_ADMIN.COPY_PAGE
title:
type: text
label: PLUGIN_ADMIN.PAGE_TITLE
help: PLUGIN_ADMIN.PAGE_TITLE_HELP
validate:
required: true
folder:
type: text
label: PLUGIN_ADMIN.FOLDER_NAME
help: PLUGIN_ADMIN.FOLDER_NAME_HELP
validate:
rule: slug
required: true
header.published:
id: move-header-published
type: toggle
label: PLUGIN_ADMIN.PUBLISHED
help: PLUGIN_ADMIN.PUBLISHED_HELP
highlight: ''
default: ''
size: medium
options:
'': PLUGIN_ADMIN.AUTO
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
@@ -1,52 +0,0 @@
rules:
slug:
pattern: '[a-zA-Zа-яA-Я0-9_\-]+'
min: 1
max: 200
form:
validation: loose
fields:
section:
type: section
title: PLUGIN_ADMIN.ADD_MODULE_CONTENT
title:
type: text
label: PLUGIN_ADMIN.PAGE_TITLE
validate:
required: true
folder:
type: text
label: PLUGIN_ADMIN.FOLDER_NAME
validate:
rule: slug
required: true
route:
type: parents
label: PLUGIN_ADMIN.PAGE
classes: fancy
validate:
required: true
name:
type: select
classes: fancy
label: PLUGIN_ADMIN.MODULE_TEMPLATE
help: PLUGIN_ADMIN.PAGE_FILE_HELP
default: default
data-options@: '\Grav\Plugin\AdminPlugin::pagesModularTypes'
validate:
required: true
modular:
type: hidden
default: 1
validate:
type: bool
blueprint:
type: blueprint
@@ -1,104 +0,0 @@
rules:
slug:
pattern: '[a-zA-Zа-яA-Я0-9_\-]+'
min: 1
max: 200
form:
validation: loose
fields:
tabs:
type: tabs
active: 1
fields:
content:
type: tab
title: PLUGIN_ADMIN.CONTENT
fields:
xss_check:
type: xss
frontmatter:
classes: frontmatter
type: editor
label: PLUGIN_ADMIN.FRONTMATTER
autofocus: true
codemirror:
mode: 'yaml'
indentUnit: 4
autofocus: true
indentWithTabs: false
lineNumbers: true
styleActiveLine: true
gutters: ['CodeMirror-lint-markers']
lint: true
content:
type: markdown
header.media_order:
type: pagemedia
label: PLUGIN_ADMIN.PAGE_MEDIA
options:
type: tab
title: PLUGIN_ADMIN.OPTIONS
fields:
columns:
type: columns
fields:
column1:
type: column
fields:
ordering:
type: toggle
label: PLUGIN_ADMIN.FOLDER_NUMERIC_PREFIX
help: PLUGIN_ADMIN.FOLDER_NUMERIC_PREFIX_HELP
highlight: 1
options:
1: PLUGIN_ADMIN.ENABLED
0: PLUGIN_ADMIN.DISABLED
validate:
type: bool
folder:
type: text
label: PLUGIN_ADMIN.FILENAME
validate:
rule: slug
required: true
route:
type: parents
label: PLUGIN_ADMIN.PARENT
classes: fancy
validate:
required: true
name:
type: select
classes: fancy
label: PLUGIN_ADMIN.MODULE_TEMPLATE
default: default
data-options@: '\Grav\Plugin\AdminPlugin::pagesModularTypes'
validate:
required: true
column2:
type: column
fields:
order:
type: order
label: PLUGIN_ADMIN.ORDERING
blueprint:
type: blueprint
@@ -1,5 +0,0 @@
form:
validation: loose
fields:
route:
type: hidden
@@ -1,62 +0,0 @@
rules:
slug:
pattern: '[a-zA-Zа-яA-Я0-9_\-]+'
min: 1
max: 200
form:
validation: loose
fields:
section:
type: section
title: PLUGIN_ADMIN.ADD_PAGE
title:
type: text
label: PLUGIN_ADMIN.PAGE_TITLE
help: PLUGIN_ADMIN.PAGE_TITLE_HELP
validate:
required: true
folder:
type: text
label: PLUGIN_ADMIN.FOLDER_NAME
help: PLUGIN_ADMIN.FOLDER_NAME_HELP
validate:
rule: slug
required: true
route:
type: parents
label: PLUGIN_ADMIN.PARENT_PAGE
classes: fancy
validate:
required: true
name:
type: select
classes: fancy
label: PLUGIN_ADMIN.PAGE_FILE
help: PLUGIN_ADMIN.PAGE_FILE_HELP
data-options@: '\Grav\Plugin\AdminPlugin::pagesTypes'
data-default@: '\Grav\Plugin\Admin\Admin::getLastPageName'
validate:
required: true
visible:
type: toggle
label: PLUGIN_ADMIN.VISIBLE
help: PLUGIN_ADMIN.VISIBLE_HELP
highlight: ''
default: ''
options:
'': PLUGIN_ADMIN.AUTO
1: PLUGIN_ADMIN.YES
0: PLUGIN_ADMIN.NO
validate:
type: bool
required: true
blueprint:
type: blueprint
@@ -1,31 +0,0 @@
rules:
slug:
pattern: '[a-zA-Zа-яA-Я0-9_\-]+'
min: 1
max: 200
form:
validation: loose
fields:
section:
type: section
title: PLUGIN_ADMIN.ADD_FOLDER
folder:
type: text
label: PLUGIN_ADMIN.FOLDER_NAME
help: PLUGIN_ADMIN.FOLDER_NAME_HELP
validate:
rule: slug
required: true
route:
type: parents
label: PLUGIN_ADMIN.PARENT_PAGE
classes: fancy
validate:
required: true
blueprint:
type: blueprint
@@ -1,104 +0,0 @@
rules:
slug:
pattern: '[a-zA-Zа-яA-Я0-9_\-]+'
min: 1
max: 200
form:
validation: loose
fields:
tabs:
type: tabs
active: 1
fields:
content:
type: tab
title: PLUGIN_ADMIN.CONTENT
fields:
xss_check:
type: xss
frontmatter:
classes: frontmatter
type: editor
label: PLUGIN_ADMIN.FRONTMATTER
autofocus: true
codemirror:
mode: 'yaml'
indentUnit: 4
autofocus: true
indentWithTabs: false
lineNumbers: true
styleActiveLine: true
gutters: ['CodeMirror-lint-markers']
lint: true
content:
type: codemirror
header.media_order:
type: pagemedia
label: PLUGIN_ADMIN.PAGE_MEDIA
options:
type: tab
title: PLUGIN_ADMIN.OPTIONS
fields:
columns:
type: columns
fields:
column1:
type: column
fields:
ordering:
type: toggle
label: PLUGIN_ADMIN.FOLDER_NUMERIC_PREFIX
help: PLUGIN_ADMIN.FOLDER_NUMERIC_PREFIX_HELP
highlight: 1
options:
1: PLUGIN_ADMIN.ENABLED
0: PLUGIN_ADMIN.DISABLED
validate:
type: bool
folder:
type: text
label: PLUGIN_ADMIN.FOLDER_NAME
help: PLUGIN_ADMIN.FOLDER_NAME_HELP
validate:
rule: slug
required: true
route:
type: parents
label: PLUGIN_ADMIN.PARENT
classes: fancy
name:
type: select
classes: fancy
label: PLUGIN_ADMIN.DISPLAY_TEMPLATE
help: PLUGIN_ADMIN.DISPLAY_TEMPLATE_HELP
default: default
data-options@: '\Grav\Plugin\AdminPlugin::pagesTypes'
validate:
required: true
column2:
type: column
fields:
order:
type: order
label: PLUGIN_ADMIN.ORDERING
blueprint:
type: blueprint
@@ -1,34 +0,0 @@
rules:
slug:
pattern: '[a-zA-Zа-яA-Я0-9_\-]+'
min: 1
max: 200
form:
validation: loose
fields:
tabs:
type: tabs
active: 1
fields:
content:
type: tab
title: PLUGIN_ADMIN.CONTENT
fields:
frontmatter:
classes: frontmatter
type: editor
label: PLUGIN_ADMIN.FRONTMATTER
autofocus: true
codemirror:
mode: 'yaml'
indentUnit: 4
autofocus: true
indentWithTabs: false
lineNumbers: true
styleActiveLine: true
gutters: ['CodeMirror-lint-markers']
lint: true
@@ -1,36 +0,0 @@
title: PLUGIN_ADMIN.MEDIA
form:
validation: loose
fields:
'types':
name: medias
type: list
label: PLUGIN_ADMIN.MEDIA_TYPES
style: vertical
key: extension
controls: both
collapsed: true
fields:
.extension:
type: key
label: PLUGIN_ADMIN.FILE_EXTENSION
.type:
type: text
label: PLUGIN_ADMIN.TYPE
.thumb:
type: text
label: PLUGIN_ADMIN.THUMB
.mime:
type: text
label: PLUGIN_ADMIN.MIME_TYPE
validate:
type: lower
.image:
type: textarea
yaml: true
label: PLUGIN_ADMIN.IMAGE_OPTIONS
validate:
type: yaml
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

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