Search the Community
Showing results for 'runtime'.
-
Hey everyone, on a recent client project we had to deal with a large number of Markdown files that needed to end up as regular HTML content on ProcessWire pages. Converting them manually or piping them through external tools wasn't an option – too many files, too tedious, and the content had to be stored as actual HTML in rich textfields, not just formatted at runtime. So we built a small module that handles this directly inside ProcessWire. How it works The module creates a file upload field (md_import_files) and a Repeater field (md_import_items) with a standard title field and a richtext body field (md_import_body) inside. The body field automatically uses TinyMCE if installed, otherwise CKEditor. You add both fields (md_import_files,md_import_items) to any template, upload your .md files, hit save – each file gets converted to HTML via PW's core TextformatterMarkdownExtra and stored as a separate Repeater item. The source filename goes into the items title, processed files are removed from the upload automatically. Template output The Repeater items are regular PW pages, so output is straightforward: foreach ($page->md_import_items as $item) { echo "<section>"; echo "<h2>{$item->title}</h2>"; echo "<div>{$item->md_import_body}</div>"; echo "</section>"; } Tag mappings One thing we needed right away: control over how certain Markdown elements end up in HTML. For example, #headings in Markdown become <h1> – but on most websites <h1> is reserved for the page title. The module has a simple config (Modules → Configure → Markdown Importer) where you define tag mappings, one per line: h1:h2 h2:h3 strong:b blockquote:aside hr:br This performs a simple 1:1 tag replacement after conversion, preserving all attributes. Works well for standalone or equivalent elements like headings, inline formatting, blockquotes, or void elements like hr:br. Note that it doesn't handle nested structures – mapping table:ul for example would only replace the outer <table> tag while leaving thead, tr, td etc. untouched. Requirements ProcessWire 3.0.0+ FieldtypeRepeater (core) TextformatterMarkdownExtra (core) GitHub: github.com/frameless-at/MarkdownImporter Modules Directory: https://processwire.com/modules/markdown-importer/ Happy to hear if anyone finds this useful or has suggestions for improvements. Cheers, Mike
-
- 5
-
-
Just released V1.4, here's what has changed/was added in the last versions: 1.1: Added companion Lister Pro (https://processwire.com/store/lister-pro/) action to translate pages using page actions. 1.2: Added backwards compatibility with PHP 8.1 1.3: Bug fixes only 1.4: Added support for the option to choose the current user language as source language, and made sourceLanguage and targetLanguage(s) hookable to allow altering them at runtime if needed
-
Hi fellow devs, this is a somewhat different post, a little essay. Take it with a grain of salt and some humor. Maybe some of you share similar experience. I don't really mean to poop on a certain group with certain preferences, but then, that's what I'm doing here. I needed to write it to load off some frustration. No offense intended. Good Sunday read :-) React Is NPC Technology Have you ever really looked at React code? Not the tutorial. Not the "Hello World." An actual production component from an actual codebase someone is actually proud of? Because the first time I did, I thought there'd been a mistake. A failed merge. HTML bleeding into JavaScript, strings that weren't strings, logic and markup performing some kind of violation you'd normally catch in code review before it got anywhere near main. "Fix this," I thought. "Someone broke this." It looks broken because it is broken. That's the first thing you need to understand. JSX is a category error. Mixing markup and logic at the syntax level - not as an abstraction, not behind an interface, but visually, literally, right there in the file - is the kind of decision that should have ended careers. Instead it ended up on 40% of job postings. And here's the part that actually matters, the part that explains everything: Nobody can tell you why. "Everyone uses it." Go ahead, ask. That's the answer. That's the complete sentence, delivered with the confidence of someone who has never once questioned whether a thing should exist before learning how it works. The argument for React is React's market share. The case for Next.js is that your tech lead saw it on a conference talk in 2021 and it was already too late. You're supposed to hear this and nod - because if everyone's doing something, there must be a reason, right? The herd doesn't just run toward cliffs. Except. That's literally what herds do. The web development community, bless its heart, has a category of decision I can only call NPC behavior. Not an insult - a technical description. An NPC doesn't evaluate options. An NPC reads the room, finds the dominant pattern, and propagates it. React is on every job posting = React is what employers want = React is what I need to know = React is what I reach for. The loop closes. Nobody along the chain asked if it was right. They asked if it was safe. Safe to put on a resume. Safe to recommend. Safe to defend at the standup. React is the framework you choose when you've stopped choosing and started inheriting. The 10% who actually think about their tools - they're out there running Alpine.js. Which is 8kb. Does the same job. No build step required. Add an attribute, the thing works. Revolutionary concept. They're running htmx, which understood something profound: the web already has a protocol for moving data, and it was fine. You didn't need to rebuild HTTP in JavaScript. You just needed to reach for the right thing instead of the fashionable one. Let's talk performance, because "everyone uses it" is already bad enough before you look at what it actually does. React ships 40-100kb of runtime JavaScript before your application does a single thing. Your users wait while React bootstraps itself. Then it hydrates - a word that sounds refreshing and means "React redoes on the client what the server already did, because React can't help it." Then they invented Server Components to fix the problem of shipping too much JavaScript. The solution: ship different JavaScript, handled differently, with new mental models, new abstractions, new ways to get it wrong. They called it an innovation. I once worked with WordPress and React together. I want you to sit with that. Two philosophies, neither of which is actually correct, stacked on each other like a complexity casserole nobody ordered. WordPress solving 2003's problems with 2003's patterns. React solving 2003's problems with 2013's patterns that created 2023's problems. Together they achieved something genuinely special: all the drawbacks of both, and none of the advantages of either. The PHP you want but in a different way and the hydration you couldn't prevent, serving pages that load like it's apologizing for something. Twenty years building for the web and I've watched frameworks rise and fall like geological events. ColdFusion, anyone? Remember when Java applets were going to be everywhere? Flash was going to be the web. Then jQuery saved us. Then Angular saved us from jQuery. Then React saved us from Angular. Rescue upon rescue, each one leaving more complexity than it cleared, each one defended by exactly the same people who defended the last one, now wearing a different conference lanyard. ProcessWire. That's what I build with. Most developers have never heard of it - which is not a criticism, that's the evidence. You find ProcessWire because you went looking for something specific, evaluated it, and it fit. It doesn't have conference talks. It doesn't have a VC-funded developer relations team. It has a forum full of people who chose it. That's a different category of thing entirely. The same 10% who finds ProcessWire finds Alpine. Finds htmx. Makes decisions that don't optimize for defensibility in interviews. Builds websites that load fast because they don't carry React around everywhere they go. There's a physics concept called a local minimum. A place where a system settles because the immediate neighborhood looks stable - the energy gradient points upward in every direction, so the system stops. Stays. Convinces itself it's home. Even if a global minimum exists somewhere else, at lower energy, lighter, simpler - you'd have to climb first, and the herd doesn't climb. React is a local minimum. The web settled here when it got tired of looking. Stable enough. Defended by enough career investment. Surrounded by enough tooling and tutorials and framework-specific bootcamps that switching costs feel existential. The ground state - simpler, faster, closer to what the web actually is - sits somewhere else, past a hill that looks too steep from inside the valley. The ground state is always simpler. That's not a philosophical position. That's thermodynamics. They don't want you to know that.
-
- 16
-
-
-
@elabx - I don't really know how those apache modules work exactly, but wouldn't they have the same issue as I realized when I thought - why not just hook into Page::render and do a string replace on all <script> tags to add in the nonce at runtime - the problem being of course that this would also add the nonce to any maliciously injected <script> tags, thereby effectively removing all protection?
-
Hi @bernhard — first of all: thank you again for RockMigrations. It has become one of those “can’t-live-without” tools in my ProcessWire projects and makes CI/CD + deployments incredibly smooth. Why I needed a workaround (shared hosting reality) In this client project I migrate pretty much everything via RockMigrations: fields, templates, roles/permissions, installed modules — and also some pages/permissions that are created dynamically based on constants/config values that change from time to time. That works great conceptually, but on a very slow shared host it led to painfully long migration runs even when there was nothing to change. A lot of my migrations are “dynamic config” style and get evaluated every deploy, which was costing me minutes per run (and that adds up quickly with multiple deploys/commits per day). A tiny helper module: hash-based skipping for config migrations I wrote a small helper module (not meant to be more than a little utility) that helps specifically with this scenario: Build a migration $config array (can include dynamic parts). Normalize + hash the config. If the stored hash for a key matches, skip running the migration. If it differs, run it and store the new hash. So migrations only execute when the effective config changed. How I use it (complete example) This is how it’s used in my migrations (example shortened, but complete in terms of usage): <?php namespace ProcessWire; use RockMigrations\MagicPage; class AuthorPage extends Page { use MagicPage; public function migrate(): void { /** @var RockMigrations $rm */ $rm = $this->wire->modules->get('RockMigrations'); $speed = $this->wire->modules->get('DlfMigrationSpeedUp'); $config = [ 'fields' => [ 'firstname' => [ 'label' => 'First name', 'type' => 'FieldtypeText', ], 'lastname' => [ 'label' => 'Last name', 'type' => 'FieldtypeText', ], ], 'templates' => [ 'author' => [ 'fields' => [ 'title' => [ 'columnWidth' => 50, 'label' => 'Display name', ], 'firstname' => [ 'columnWidth' => 50, ], 'lastname' => [ 'columnWidth' => 50, ], ], ] ] ]; $key = 'template:AuthorPage'; if ($speed && !$speed->shouldRun($key, $config)) { // unchanged -> skip } else { $rm->migrate($config); if ($speed) $speed->remember($key); } } } Important limitations (when this does not apply) This only works if the migration outcome is fully determined by the config you hash. It’s not a good fit if relevant parts can be changed outside code, for example: field/template settings changed in the PW admin GUI, manual permission tweaks, any DB drift that is not represented in the hashed config. In my case it’s safe because I enforce “everything via RockMigrations/API” for this instance — but it’s definitely not something I’d blindly recommend for every setup. Performance impact (real numbers) On that shared host, some of my “dynamic config” migrations were extremely slow even when they effectively had nothing to change. Example excerpts from one run before the helper (I’m anonymizing the real class names here): Before: TemplateXPage::migrate() 53.434s TemplateYPage::migrate() 29.347s TemplateZPage::migrate() 20.431s …many more similar steps… Total runtime in that log was ~226s. With the helper enabled, unchanged migrations are skipped and return immediately: After (unchanged config): TemplateXPage::migrate() 0.000s TemplateYPage::migrate() 0.000s TemplateZPage::migrate() 0.000s …everything essentially no-op… Total runtime (from the log): ~0.72s. That’s a night-and-day difference for repeated deployments where the effective config didn’t change. Code (module) on Gist I didn’t want to paste the whole module here; the code is in this Gist: https://gist.github.com/fsnck/cf7799c1f761681cc5f0dea13e3b802a If anyone has thoughts on edge cases or improvements, I’m happy to discuss. And again: thanks @bernhard — RockMigrations is awesome.
-
@elabx you might want to use this approach and create a dynamic runtime superuser: https://github.com/baumrock/RockShell/blob/64a453a518e11dc445ae451bf1ca2d5e30126ef6/App/Command.php#L588-L590 $su = new User(); $su->addRole("superuser"); wire()->users->setCurrentUser($su);
-
I’m working on a custom runtime markup field in ProcessWire’s admin. This field loads DOM elements asynchronously after the page loads (via JavaScript), updating part of the form visually. The problem: When I try to leave the page without actually changing any form inputs, the "There are unsaved changes, are you sure you want to leave?" prompt still appears. I suspect ProcessWire's admin JavaScript is detecting these DOM changes, but I’m unable to find which JS object or event is responsible. Can anyone share how ProcessWire’s admin detects changes or how to override/reset that state? Or pointers on what JS objects or events to manipulate? Thanks a lot in advance!x
-
ProcessWire Commerce: Help Needed
Tiberium replied to kongondo's topic in ProcessWire Commerce (Padloper) Support
I use the Docker image from solidnerd (https://github.com/solidnerd/docker-bookstack) witch Docker Compose. Where I stumped was, that you have to set some settings as environment variable not in a .env-file, but give it docker on runtime. My hole setup is on a v-server with portainer and the nginx proxy manager. Bookstack (+MySQL) and NPM share the same network, what makes the configuration on the NPM side easier. Setup as a "stack": The config with the setup of the environments variables: Created containers from the stacks above: The in before created network "proxyable": Hope the little inside example help. Updating is super easy. You update the image and then let the container rebuild on the latest. -
What are you suggesting with the question, that we should drop dark mode? But to answer your question, I would say the same thing you'd do in any issue situation, report the issue. But by the time this is on the main/master branch, I don't think there will be any remaining concerns about dark mode. In any case, we already have options for disabling dark mode in the module configuration, in your /site/config.php file, or dynamically at runtime, enabling you to disable dark mode entirely, or just in specific cases. It seems like we already have a lot of options for those that aren't keen on dark mode.
-
Larger files uploads are not compleating
BrendonKoz replied to Mark_invisu's topic in General Support
Do you know where your host's support was originally able to get the server and/or PHP error logs that originally showed the issue with the core ProcessWire file's exception? You mention no errors are showing up in the logs, but the logs from the ProcessWire admin/backend may not be able to store the error message if it's happening at a lower level in the stack. The browser dev tools can be helpful to some degree, but it's not the only place to look, nor is the PW admin. If you're not seeing any useful error logs in those two places, have you looked (or been able to look?) at the server logs, whether those are NGINX/Apache, or PHP (runtime) logs? It seems you may be getting different responses depending on the host you're on (you mentioned both a 501 and 504 timeout errors), so having any sort of definitive logs to look at, if they exist somewhere, would be awesome. -
Hey all. This year I launched a new website for Modernism Week, a nonprofit located in Palm Springs, California that celebrates midcentury architecture and hosts engaging educational annual events. https://modernismweek.com This is my most complex ProcessWire website to date and I wanted to share some of the things that were implemented behind the scenes. Project Requirements When I met with stakeholders at the organization, their website was a 5 page brochure site with minimal content and links to buy tickets. The ticketing platform they use is robust, but doesn't provide an experience you would expect from an event this significant. The project was a blank canvas and after an assessment I provided a list of features that would benefit the organization, address shortcomings, identify opportunities, and support future growth and business goals. These included: A vibrant design that mirrors the architecture they celebrate to create a more inspiring experience for visitors Implement stronger adherence to brand standards and new branding to celebrating the 20th anniversary of Modernism Week Feature dedicated pages for events, activities, and offers Promote event and organization sponsors Challenges At first glance, the site looks pretty standard however, it features a full event section that displays everything visitors can do at each activity during the 10 day event. Leading up to and during the event, the information changes to stay up to date with newly added activities and updates to locations, ticket availability, descriptions, pictures, etc. During planning we identified unique challenges. Annual site growth of over 300 new pages for activities and events on top of any normal site growth Reduce the amount of work needed to initially enter the information for each event Add complexity without compounding increases in work to maintain actively changing information Maintain content accuracy by mirroring regularly changing information that is available on pages located on the ticketing platform Synchronize event and activity information with a ticketing platform that does not provide a public API Allow visitors to browse activities when previewing them prior to the ticket sales launch day Keep cross-referenced content across the site, such as promoted activities and offers, current Provide an integrated workflow between people managing the ticketing platform and maintaining the website Improve SEO and implement structured data derived from cumulative page content for all activities Ensure that all activities in an event are centrally searchable and filterable not only when using whole-website search, but within a dedicated activity search feature for each event Deliver a fast and performant experience for visitors Manage server and database loads to prevent overloading resources or rendering the site or admin unusable Oh, and the deadline was the day tickets went on sale, on a Friday, where traffic spikes to thousands of users per minute. We broke every rule in the book. Manually entering and managing each page is a task too large for one or even two people. Events and activities must be added to the site while they are simultaneously added to the ticketing platform in the months leading up to events. The amount of overhead for project management and task delegation would overburden a team already working tirelessly to successfully put on an event of this size. The timeline to design, build, test, and deploy the site was only 4 months. The solution was automation. The Application Layer The only way that the site could effectively and successfully be managed accurately is to automate tasks and consolidate complex processes into single-button clicks and cronjobs. This is where ProcessWire really flexes its muscle. The API and practically limitless flexibility in custom implementation provided the ability to build out everything that was required. The site has its own custom module that provides an interface for executing back end work to synchronize data between the ticketing platform and the site. The site also employs over 60 custom hooks and hook-supporting classes (some of them I shared here) that make chained and background actions possible. As mentioned, the ticketing platform does not provide an API so there was no conventional method to easily retrieve data. This means that all of the data pulled from the ticketing platform is pulled via web scraping. To more easily and accurately retrieve this data, I built a separate internal tool for the ticketing team where they enter activity information, pre-formats it for them to a design, and adds encoded data attributes on elements in the markup that helps make scraping more accurate. The data this retrieves is then parsed, cleaned, and fed through processors that compare, convert, and input into ProcessWire where pages are created or updated. The RoachPHP library facilitated scraping and it's great. If you're interested in web scraping, it's a great tool. Performance There are a lot of relatively expensive operations required to sync activities in large numbers. The way that unique data is stored, such as ticket pricing, availability, and schedule requires more complex fields like RepeaterMatrix. Often the data required to calculate or display information makes using selectors alone to query/filter data difficult, inefficient, or not possible. I chose to use RepeaterMatrix fields over individual pages because of the the reduction in complexity when managing the schedule, pricing, and ticket availability. Some activities occur once, other occur multiple times per day over the course of 10 days. I fully expect that the amount and complexity of information stored will increase over time due to future improvements and feature implementations. Being able to manage all of this data, compare it, and adjust it one one page is important for accuracy and time spent on task when manual edits need to be made and cross referencing multiple pages would be cumbersome. Regardless of page or field choices, nested loops of up to 200 pages involves significant server loads, especially when that process may involve resizing images to multiple dimensions and generating required output data from multiple fields. A good example is the live search feature. Thousands of visitors querying the database is not feasible for even the reasonably powered VPS it's on and I wanted to create something closer to "live search". To address the workload required, the search modal is empty on page load and a separate page is used on the back end that is separately cached using ProCache. This allows one page to serve modals being cached with every page. If the modal content was cached with all activity pages then event changing the ticket availability, title, summary, image, etc. would require expiring the cache of over 200 pages that have complex data. Opening the "All Activities" modal executes an htmx request, and the cached modal contents are loaded near instantaneously. The slowest part of that is the animating the loading animation in/out. Underneath the hood, the live search/filter uses keywords that are embedded in data attributes on each row. These keywords are taken from multiple fields, parsed, stripped/prepared for filtering, and combined. Doing this in a loop is noticeably slow to perform every time the page is called. Filters for categories. sold out, activities happening today, search by keyword, and viewing activities on a specific day is all done on the client side and require calculating per-day ticket availability, first and last occurrence dates, and during the event whether an activity has passed- which when displayed overrides a sold out status. Expiring this page is still an expensive task and calculating the dates, times, and statuses to produce the data needed has taken up to an unacceptable 6+ seconds. If too many pages are not cached this can cause performance issues for multiple users on multiple pages. It's not just about front end performance, it's also important to think about admin performance. To remedy this, I implemented a type of field pre-caching. Front end performance is entirely dependent on ProCache. Without it the server load would be far too high and performance hits would affect time on site and ticket sales. Overall, the goal is to touch the database as little as possible. ProcessWire is indeed fast, but there are always limitations across many different moving parts. Synchronizing Event/Activity Data Importing data can involve multiple background HTTP requests, reading and updating high numbers of pages and dozens of repeater pages and fields. Syncing is done by scraping pages on the ticketing platform. There may be instances that after analyzing the data retrieved does not require updating a page because there were no changes. If there are no changes then the page shouldn't be saved. Rather than check all of the fields for changes in values, data is hashed and stored for later comparison. Every activity page can be updated from the ticketing platform via either the main event page that summarizes useful data, or the individual ticketing page. Sync can be enabled or disabled for the activity entirely, if it is enabled, then sync can be toggled on/off for every individual field in case local management of data is preferred. The retrieved data is hashed and compared to the page hash value. If the last sync datetime is older than the time limit set in the event settings, or the hash does not match, the the page is updated and saved. If not, the page is then ignored. This saves a lot of processing time, power, and database hits while maintaining state. Example of an activity page. Ticketing occurrence data is parsed and run through regex to extract individual data and formatting for dedicated fields. The three fields on the bottom are taken from the Instances Summary field when parsed on import. Looping within loops to create renderable values for the front end is taxing. But using custom page classes to organize business logic with methods that handle processing, formatting, and caching data are immensely powerful and make short work of interacting with pages when bespoke functionality is required. Because of the high number of hooks on various operations, the ability to save quietly without triggering them is used extensively as an efficiency measure and also to prevent unwanted side effects when operations do not call for them. Sync uses this feature the most. Field Pre-Caching A global field called "Field Support Metadata" is used to move significant amounts of data processing from the page view event over to the page save event. This takes the multiple field value processing and image sizing out of loops for rendering and takes the extra time on the back end operations side. The activity page example above uses this method almost entirely for front end rendering and makes pre-calculated values accessible in one field. This was inspired by the SearchEngine module by @teppo that uses a single field to cache values for search. It really saved my bacon 🤣 big thanks! Page data is saved as JSON and then retrieved on demand. Metadata includes a wide array of values. Because it's JSON, adding additional data later is very easy. While i could have used the the $page->meta feature, being able to check the contents of the field visually in the admin without dumping or testing values has been useful. All of that data is processed and stored on a Pages::saveReady hook using methods defined on the custom page class. This process is skipped entirely if the activity has no occurrences scheduled. <?php wire()->addHookAfter('Pages::saveReady(template=event_activity)', function(HookEvent $e) { $page = $e->arguments('page'); $occurrences = $page->activity_occurrences; if (!$occurrences->count()) { return; } $firstOccurrenceDate = $page->firstOccurrenceDate(fromMetadata: false); $lastOccurrenceDate = $page->lastOccurrenceDate(fromMetadata: false); $timezone = wire('config')->timezone; $allOccurrenceDateTimeStart = array_map( fn ($date) => CarbonImmutable::parse($date)->setTimezone($timezone)->toDateTimeString(), $occurrences->explode('date_time_start'), ); $page->pushToFieldMetadata([ 'totalOccurrences' => $occurrences->count(), 'totalInstances' => array_sum($occurrences->explode('number')), 'firstOccurrenceDate' => $firstOccurrenceDate?->toDateTimeString(), 'lastOccurrenceDate' => $lastOccurrenceDate?->toDateTimeString(), 'isSoldOut' => $page->isSoldOut(fromMetadata: false), 'isUncategorized' => $page->isUncategorized(fromMetadata: false), 'ticketingUrl' => $page->ticketingUrl(fromMetadata: false), 'activityDates' => $page->activityDates(fromMetadata: false), 'allOccurrenceDateTimeStart' => $allOccurrenceDateTimeStart, 'listFilterKeywordString' => $page->listFilterKeywordString(fromMetadata: false), 'structuredData' => $page->getStructuredData( includeOffers: false, includeLocation: true, includeOrganizer: false, includeDescription: true, includeImages: true, includeParentEvent: false, fromMetadata: false, ) ]); }); Editing one hook makes adding or modifying pre-processed data available everywhere on demand at runtime via one field which is pretty nice. The 'fromMetadate' set to true will return cached values, if false, it will return values that are generated from field data. This allows for the same method to be used anywhere in the application with control over the data source. Any data that can be memoized is via the page class so that if a value is calculated at runtime (wasn't yet cached to metadata yet for some reason) and accessed more than once during a request/response loop then it will be pulled from memory. Granular ProCache Control Not every field changed should invalidate cache. For example, if sync is toggled on/off, that does not affect the rendered page so the cache for that page should not be cleared. Page save events for activity pages fire ProCache hooks that analyze which fields on the page have changed and only clear the cache if it will affect pages that rely on it. Activities are referenced elsewhere and added as featured items to help promote them on other parts of the website. So if an activity page is saved where the title is changed and that activity is featured on various other pages, that could clear that page, the Event page, the All Events modal page, the category page for that activity, and the home page. Using hooks allows for very specific cache operations that cannot be configured in the admin UI. Example of how activities can be featured and promoted in many places elsewhere on the site. There's a little more about this on a ProCache support thread I posted to discuss efficient caching strategies. Thanks to @ryan for the insight and recommendations 👍 Protip: Pre-Warming Your Cache ProCache is excellent but it can't cache a page until it's visited. You can seriously speed this up by using quicklink. Quicklink watches the page for links as they're scrolled into view where the browser makes a background prefetch request and caches the HTML document locally. If ProCache has already cached the page, the background request/response is so quick that the browser pulls every link on the screen near instantly. When you click on the link, you get response times like this. Which aren't response times, they're the speed that the browser cache operates locally. Total insanity. And the nice thing is that if they don't visit the page, they've kindly cached it for everyone else. Thank you, kind visitor. Dynamically Generated Content One of the features built in thanks to hooks is automatic content generation. Examples are the "more in category" and "you might also be interested in" type suggestions at the bottom of every activity page. This is designed to help visitors explore other things that they might like. This provides a great navigation experience on the site that can also increase ticket sales. These are automatically selected when a page is created during first sync/import using a Page::saveReady hook. Activities are given tags on the ticketing platform to help organize them. These are pulled during import, analyzed, and activities are automatically selected by ranking the most similar according to matching tag count. This boosts usable content on the page with zero work by a human. While they're automated, they can also be changed or re-ordered when editing an activity page. You can delete any or all of them and they'll be repopulated with new activities when the page is saved. The code that populates activities lives in a custom page class method for the activity so it can be called anywhere. Future Features The site as it stands currently is not in it's final state. Some planned features had to be cut due to prioritizing event and activities which are revenue generating. Future features include: A full robust blog that delivers a magazine-like experience which also requires importing posts dating back to 2015 from a separate WordPress site. An improved workflow that provides partners the ability to submit information about their activities for events via forms, integrates that tool the ticketing team now uses while adding the ability to save their progress and pre-create ProcessWire activity pages pages to reduce the overhead of major data import operations on the site. Adding a separate section that contains a web app that attendees can use during activities for additional educational information while at the event Create an "itinerary" feature that lets visitors browse the site and add activities that they plan to purchase tickets for. Use this feature to send email reminders when tickets are available and marketing leading up to the event Possibly implement an event calendar that lets people get an overview of the schedule and avoid schedule conflicts. A "Past Events" section of the site where events are moved to but can still be browsed. Where It Can Be Improved There are places in the code that should be using selectors rather than pulling larger numbers of pages since selectors and queries are optimized and efficient. Having a short deadline takes a toll on planning and execution. Offloading more to cronjobs. I had to keep a lot of operations manually triggered that I want truly handled by background automation but I needed an opportunity to analyzej real-world performance in production and have the chance to review the quality and accuracy of the data imported. Scraping is great, but sometimes it takes fine tuning to get things right. Not to mention the amount of regex used vs my skills with regex... More robust features for browsing activities. More filters and options to choose which activities to browse. A lot of great ideas have come out of living with the site and there are some basics I'd like to see implemented. At its heart, this site is still a website but ProcessWire really shines when it's given a big job. It eliminates the need to consider a full application framework and is faster to develop for when you need strong core features for content management. As you can imagine, trying to build something like this on a platform that is not as developer oriented as ProcessWire is would be very not fun and the high quality Pro modules, community modules, and outstanding API made it possible to plan and execute without compromise. If you've made it this far, thanks for reading!
- 1 reply
-
- 17
-
-
-
Glad that you got the install working. Thanks for informing me about this. Just pushed an update, should be fixed now. PAGEGRID block modules can have dependencies that they download automatically (eg. the image block donwloads the SVG File Sanitizer/Validator Module). Ryan added a new check with the latest updates that needs a setting in config to allow downloads. But I found a way to set this at runtime when the user is superuser und debug is on, so should work now even without setting it explicitly. You are on the right track. PAGEGRID is not doing anything special when it comes to multilanguage. You only need to install all the core multilang modules and set the fields to multilang like you did. You can also test if multilanguage works for other non PAGEGRID pages and make sure it's not relates to PAGEGRID. Also you can click the edit icon (pen icon) when you hover the text block. In the sidebar you should see your textfield with the multilanguage tabs. The buttons at the top of your PAGEGRID field are switching the whole site to the corresponding language so you can quickly edit the text with the inline editor. What happens if you click those buttons?
-
Hey there! I posted this before I took advantage of a feature in RockPageBuilder to manage globally sharable settings for blocks in RPB. @bernhard has since added some very nice features that make my tips below a little out of date. I recommend viewing his comment below and taking advantage of RPB's new native solution. Thanks to Bernhard for his ongoing work of making RPB event better for developers and users 👏 ---------- Sharing a strategy that I have found useful when working with blocks and block settings. I use the block settings feature heavily and in many instances most blocks have some of the same settings fields. To help make managing these easier, I created a workflow to help me manage these and it's saved me a good amount of time while being able to reuse settings between projects very easily. In my case, some of these are: Spacing between blocks/sections on the page Background/accent colors Content location within blocks Block presentation My approach is to create a dedicated class with static methods that return a settings array. Example: <?php // /site/init.php /** * This creates a namespace for a /site/templates/RockPageBuilderSupport directory */ wire('classLoader')->addNamespace('RockPageBuilderSupport', __DIR__ . '/templates/RockPageBuilderSupport'); This file contains the fast helper methods that create settings fields <?php // /site/templates/RockPgeBuilderSupport/BlockSettings.php namespace RockPageBuilderSupport; use ProcessWire\RockFieldsField; /** * Reusable block settings */ class BlockSettings { public static function sectionPresentation( RockFieldsField $field, array $config = [], array $additionalValues = [], ): array { $name = 'section_presentation'; return [ 'name' => $name, 'label' => 'Section Presentation', 'value' => $field->input($name, 'select', [ '*normal' => 'Normal', 'standalone' => 'Standalone', 'standalone_drop_shadow' => 'Standalone + Drop Shadow', ...$additionalValues, ]), ...$config, ]; } public static function bodyWidth( RockFieldsField $field, array $config = [], array $additionalValues = [], ): array { $name = 'body_width'; return [ 'name' => $name, 'label' => 'Body Width', 'value' => $field->input($name, 'select', [ '*constrained' => 'Constrained', 'full' => 'Full', ...$additionalValues, ]), ...$config, ]; } public static function backgroundColor( RockFieldsField $field, array $config = [], array $additionalValues = [], ): array { $name = 'background_color'; return [ 'name' => $name, 'label' => 'Background Color', 'value' => $field->input($name, 'select', [ '*white' => 'White', 'seafoam' => 'Seafoam', 'champagne' => 'Champagne', 'none' => 'None', ...$additionalValues, ]), ...$config, ]; } // ...as many commonly used settings methods as you need } Settings are easily reusable in any block. <?php declare(strict_types=1); namespace RockPageBuilderBlock; use ProcessWire\RockFieldsField; use RockPageBuilder\{Block, BlockSettingsArray}; use RockPageBuilderSupport\BlockSettings; class BlogFeed extends Block { const prefix = "rpb_blogfeed_"; /** * Block config info */ public function info(): array { return [ 'title' => 'News Article Feed', 'description' => 'An array of blog posts', // ...other info ]; } /** * Runtime block settings */ public function settingsTable(RockFieldsField $field): BlockSettingsArray { $settings = $this->getDefaultSettings($field); $settings->add( BlockSettings::bodyWidth($field) ); $settings->add( BlockSettings::bodyLocationVertical($field) ); $settings->add( BlockSettings::actionLocationVertical($field, config: [ 'label' => "Below 'view all' link" ]) ); $settings->add( BlockSettings::sectionPresentation($field, additionalValues: [ 'full' => 'Full news feed design', ]) ); return $settings; } // ... ommitted for brevity } This has helped me easily manage common settings for 20 different blocks by editing the settings in one place. By adding the $config and $additionalValues parameters to the BlockSettings methods, overrides can be added when individual blocks need customization. Thanks to the runtime nature of block settings, making these changes on the fly is extremely easy. This has saved me a lot of time and helps keep things organized. I was able to carry over the bulk of my work with settings from one project to another and it really helped out a lot. Would love to hear if others out there have developed some tips and tricks that help you build your sites!
-
@artfulrobot I'm not really sure why it breaks the ability to type, it might be something with my JS that needs adjustment. But placing the icon before the label may still be an issue, so maybe an option to place it after would help for those cases. I also like what you mentioned about select and unselected optgroups, and am going to add this option. We'll lose the ability to use optgroups for their original purpose, but that seems like an acceptable tradeoff if that option is enabled. I might also add an option for separate selected and unselected selects (2 side-by-side selects), which would leave the original optgroup ability. The biggest issue I get when dealing with anything select related is how to keep it functional in iOS, since iOS Safari seems to be pretty inconsistent in how it recognizes changes to options are runtime. Android/Chrome seems to work more consistently.
-
I'm not sure about this. That should only be the case on regular runtime migrations, but when doing a modules refresh all migrations should fire. I'm using RM Deployments and they perform a backup before each deployment. Is that what you are looking for?
-
Hey @herr rilke thx for your interest. Unfortunately nobody else showed interest so it looks like a video would not be interesting to a lot. I did that as well, but what I don't like with that approach is that if you move a block you have to adjust all classes of all blocks which is tedious. My approach works similar but different 😛 I use a dedicated block type solely for the color. This block sets a custom runtime flag (like $site->bgColor) and all blocks then add a custom class based on that bgColor flag's value. So once I have a color block with setting "muted" all following blocks might get the class "bg-muted", for example. Then when the next color block comes up it might set the bgColor flag to "primary" which would tell all following blocks to add the class "bg-primary". Now the challenge is to make that work with spacings and with frontend editing. On frontend editing the problem to solve is with sorting blocks, because classes come from php and sorting is done on the client with JS. So you need to add a little js to adjust classes once sorting happens. In my project this is done like this: // section color updates on sort document.addEventListener("rocksortable-added", (event) => { let sortable = event.detail; sortable.option("onMove", (e) => { setTimeout(() => { let el = e.dragged; let container = el.closest("[sortable]"); let children = container.children; let colorClass = null; // loop all items and update color classes for (let i = 0; i < children.length; i++) { let child = children[i]; // don't change class of dragged item if (child.classList.contains("sortable-drag")) continue; // update colorClass on every rpb-color block if (child.classList.contains("rpb-color")) { colorClass = child.getAttribute("data-col"); continue; } // update colorclass of element if (!colorClass) colorClass = "white"; child.classList.remove("has-bg-white"); child.classList.remove("has-bg-muted"); child.classList.remove("has-bg-primary"); child.classList.remove("has-bg-secondary"); child.classList.add(`has-bg-${colorClass}`); } }, 0); }); }); For block spacings I've switched my approach from PHP based calculations to CSS based calculations. The trick is to only use margin-top for blocks, because then you can define different margins for blocks based on the previous block like this: .bg-white + .bg-muted { margin-top: 20px; } This way you can achieve a lot of flexibility in your design with very little complexity in code. Hope that helps!
-
TinyMCE itself supports this via the style_formats setting, where you would define a "class" value rather than a "classes" value, e.g. style_formats: [ { title: 'Table row 1', selector: 'tr', class: 'tablerow1' } ] But PW's implementation of TinyMCE doesn't provide a way to directly control this setting and instead parses the contents of the "Custom style formats CSS" config textarea into the style_formats setting. Situations like this are why I think PW should provide a hookable method allowing the array of data that becomes the TinyMCE settings to be modified at runtime, as mentioned in this issue: https://github.com/processwire/processwire-issues/issues/1981
-
I came back to the thread to share some experiences and they are very similar to what @sebibu is running into. Latte introduces a lot of challenges with scoping and augments how you interact with ProcessWire. This is most pronounced for me when working with different inheritance types provided by Latte. When working with `include` there is no scope so anything that is added using {include 'file_name.latte'} will have access to the parent variables. This includes $page and $wire. Unfortunately the limitations with Latte is that you can't define blocks inside them, they only take parameters in the {include} statement. This is fine for most applications, but if you have a reusable component, such as a modal where the markup for the modal stays the same but complex contents should be contained in that markup, it would best be served by using {embed} because you can define something like {block content}{/block}. So while variables are available in primary template files, using all available features in Latte starts to lock out ProcessWire. Files included using {embed} are isolated in scope, so anything you want/need available in that file must be passed as parameters or added as rendered content within a {block}stuff here{/block}. This can get laborious for developers that are used to having ProcessWire's global variables and functions available anywhere. From my experience, there are two options. {embed 'file_to_embed.latte', page: $page, wire: $wire, foo: 'foo', bar: 'bar } {block content}waddap{/block} {/embed} You can choose to add parameters to an {embed} file that provide ProcessWire's API. I'm not entirely a fan of this because it feels laborious and if we're just going to have to pass the ProcessWire API into Latte components to overcome the limited scope, then it's just an extra step that ends up adding additional parameters passed to components all over your templates very repetitiously. I'm also used to working around the concept of only providing objects the specific data they are supposed to work with like best practices for dependency injection containers. The second option is to add custom functions to Latte using a hook at runtime. This method allows you to declare functions that will be available globally within all types of Latte reusability methods. Here's an example of a hook file on my current project. I have a dedicated latte_functions_extension.php file where I can declare these in an organized way. <?php declare(strict_types=1); namespace ProcessWire; use Latte\Extension; final class CustomLatteFunctions extends Extension { public function getFunctions(): array { return [ // Latte templating paths 'definitions' => fn (string $file) => $this->createComponentPath('definitions', $file), 'embeds' => fn (string $file) => $this->createComponentPath('embeds', $file), 'imports' => fn (string $file) => $this->createComponentPath('imports', $file), 'layout' => fn (string $file) => $this->createPath('layouts', $file), 'partial' => fn (string $file) => $this->createPath('partials', $file), // Expose ModernismWeekSite.module.php as site() 'site' => fn () => wire('modules')->get('ModernismWeekSite'), // Ensure that wire() is available in all components 'wire' => fn (?string $property = null) => wire($property), ]; } /** * Creates a path for including a component * @param string $file Dot notation subdir and filename, * @return string */ private function createComponentPath(string $componentSubdir, string $file): string { return $this->createPath("components/{$componentSubdir}", $file); } /** * Creates a component file path for a given filename does not require .latte suffix * @param string $templatesSubdir Name of directory in /site/templates * @param string $file Name of .latte file that exists in the directory */ private function createPath(string $templatesSubdir, string $file): string { !str_ends_with($file, '.latte') && $file = "{$file}.latte"; return wire('config')->paths->templates . "{$templatesSubdir}/{$file}"; } } $wire->addHookAfter( "RockFrontend::loadLatte", fn (HookEvent $e) => $e->return->addExtension(new CustomLatteFunctions), ); I've defined a 'wire' function that will be available in every component with a parameter that allows you to use it like you would expect to such as 'wire('modules')'. I have a custom site module so I've exposed that as 'site()'. If you wanted to make it easier to work with modules in your templates and included files you could define a more terse 'modules' function: <?php final class CustomLatteFunctions extends Extension { public function getFunctions(): array { return [ // ... 'modules' => fn (string $moduleName) => wire('modules')->get($moduleName), ]; } } I feel that there is a tradeoff when using Latte in ProcessWire. There are some great features in Latte, but it requires introducing abstractions and feature management to make Latte act like ProcessWire, like manually defining functions. This just means that you'll have to keep a balance of complexity/abstraction vs. using as minimal enough of a approach to keep it sane. The other challenge here is that now there can be a deviation between where the native ProcessWire API is used in Latte and other places where it isn't. So some files will use $modules, and other files will use modules(), and it's not clear whether that's referencing the ProcessWire functions API, or whether it's leveraging Latte's custom functions extension feature. Something to keep in mind when determining how other files will be included/rendered in other files. In my case I have two examples that brought this challenge out for me today. Here's one // Native behavior provided by the module // Does not work everywhere due to scoping in Latte. This caused an issue when trying to embed forms // in a modal within a {block} {$htmxForms->render($page->field_name)} // With one level of abstraction using a custom function // Because this replicates how ProcessWire provides the wire() function natively, the usage feels // natural and predictable, especially for core behavior, but this introduces a lot of verbosity // that starts to make files pretty messy {wire('modules')->get('FormBuilderHtmx')->render($page->field_name)} // With two levels of abstraction in Latte via a custom function // This looks better and still adheres to the syntax of the ProcessWire functions API // The issue is that every native ProcessWire function has to be manually replicated in our custom // functions hook class. Managing this in the long term requires extra work and cognitive load {modules('FormBuilderHtmx')->render($page->field_name)} // With 3 levels of abstraction // This has restored the feel of the variables provided by the module, but again we have to track // And implement them on an as-needed basis to manage them within the context of usage in Latte {htmxForms()->render($page->fieldName)} The level of abstraction you choose depends on how much customization you want vs. how much extra work it will take to maintain simplicity by hiding complexity. The other functions, 'embeds', 'definitions', 'imports', etc. are to overcome the relative paths all over the place in Latte. // In my home.latte file {layout './layouts/main.latte')} {var $collapseNavOnScroll = true} {import './components/definitions/headlines.latte'} {import './components/definitions/event_activity_card.latte')} {block subnav} {embed './components/embeds/event_subnav.latte', eventPage: $page->eventPage()}{/embed} {/block} // ...etc // Becomes {layout layout('main')} {var $collapseNavOnScroll = true} {import definitions('headlines')} {import definitions('event_activity_card')} {block subnav} {embed embeds('event_subnav'), eventPage: $page->eventPage()}{/embed} {/block} // etc. // In RPB blocks {embed '../../../components/embeds/example.latte', content: $block->body()}{/embed} {embed embeds('example'), content: $block->body()}{/embed} This really helps when working with Latte templates that embed components that have nested embeds and imports because the functions are generating absolute paths that Latte can handle. With these functions, I don't have to think about relative paths anywhere. As for the directory structure that I chose that requires the different paths, here's what it looks like: /templates ...etc /components /definitions /embeds /imports ...etc I chose that method because Latte has many different ways of including, embedding, and importing code from other files. It made more sense to organize my code by how Latte treats it. It wasn't my first choice, but this overcomes confusion that I was experiencing when working with all of the files sharing the same components directory. Without this type of organization it can be challenging to because of scoping and how {embed}, {include}, {define}, and {import} behave differently. Some accept parameters, export values, or use blocks, but not all of them do. So having a "modal.latte" component file that provides a {block content}{/block} next to 'button.latte' that doesn't render anything and only exports items created using {define}, next to a file that is only added to a template using {include} that doesn't take parameters or provide blocks had me jumping between files a lot and slows down development checking to see that the file is being used correctly. Just sharing some of my experiences in case it helps anyone else out. If anyone sees anything here that can be done better or if I'm missing out on features that Latte has that don't require some extra steps, let me know!
-
New blog post: Introducing the Custom Fields Module
FireWire replied to ryan's topic in News & Announcements
So this is actually something I cooked up to "mark" certain subfields within the custom field and pretty much the entire cause of this approach I took. Your include tip is pretty ingenious, I never would have thought of that. These are good tips. As for the use case of addClass, it's definitely a hacky method of attaching some additional data to each subfield. I have a Pages::saveReady hook that loops through groups of fields on the page and creates a supplemental index field for the SearchEngine module. It's only necessary for fields that aren't compatible with the module out of the box. I needed a way to indicate which subfields of custom fields shouldn't be added to the search index so in the custom field config I added 'input:nosearchindex'. It's just a workaround to indicate which fields should be excluded at configuration but which wouldn't be known at runtime. Really appreciate the response, very helpful! -
New blog post: Introducing the Custom Fields Module
FireWire replied to ryan's topic in News & Announcements
@ryan Is there a way to make the custom field children iterable? I have a script that loops through a couple of fieldtypes on a page where I don't know the names of the custom field children at runtime. My solution requires a little digging to access the '_custom_property_name' by drilling down into $customFields->defs()->getInputfields()->children() then looping through the subfield defs and accessing the value using $page->{$customField->name}->{$subfieldDef->_custom_property_name} I might be missing a better way to do that. I'm coding so much that my brain is going to melt and might have missed the right way to do it. -
Request for Input: What features should a PW calendar module have?
monollonom replied to bernhard's topic in Modules/Plugins
It’s an interesting mix indeed. Would you say these copies act as aliases but with their own date? Does it require a special template or is it that your module dynamically add a page reference field to the copies and then your module add hooks to get the main event’s data at runtime? What is very nice is it makes it super easy for the editor to edit a specific copy. Among many other benefits 🙂 The only downside I can think of in your setup is what if a recurring event gets cancelled? Is there a way to easily delete the copies? If you have a page reference field it shouldn’t be too hard actually... Maybe? If I rephrase what’s happening: you create the event, set its date to "recurring" and go ahead and create your copies. Then you save the page. If you edit it again, you have the "recurring" date type selected with the options to create new ones, but no indications of the copies previously created. Is this correct? -
Request for Input: What features should a PW calendar module have?
bernhard replied to bernhard's topic in Modules/Plugins
First milestone reached!!! 😍😍 Yesterday I spent the whole day with refactoring the JS of the rrule gui and I added some more options to cover hopefully 99.9 of all necessary scenarios. The gui now has a toggle to switch between simple and advanced mode to keep things as clean as possible and not overwhelm the users for simple rrules like "every week for 4 times". At the moment 80 hours have gone into this module (excluding the daterange input, which was already done some time ago). 💪🤯 Here my answers to the open questions: I don't think there is an ideal solution to this problem. I even found a bug in google calendar when comparing and playing around!!! I decided against a fake "never" option, as it does something different than what the UI shows. So in my case if the user does not enter an end date or a max number of events it get's limited to 100. This limit is configurable in the module's settings, though 😎 I've also added the dates of the first and last event beside the resulting rrule to make it clear what is going to happen. Additionally the user will get a table of all events that will be created. Sometimes - but not always! rrules are tricky 😄 - the first occurrence is the main event itself. In that case the table shows a hopefully clear indicator: Yes and no, I'm using a mix to somehow get the best of both worlds. I'm creating real pages for the events, but I'm only populating the date field. All other fields (like shown with the title in the video) will be inherited from the main event at runtime. This makes it possible to have complex event descriptions (like one built with RockPageBuilder) with images etc. and when creating 1000 recurring events it will still only consume the space of one event + 1000 date inputs (but not 1000 copies of images). I hope the video answers that? 🙂 -
Remove field from page edit or any repeater field within it.
elabx posted a topic in General Support
I had though of this hook: $wire->addHookBefore("ProcessPageEdit::buildFormContent", function ($event) { wire()->addHookBefore('Inputfield::render', function ($e) { $inputfield = $e->object; if (strpos($inputfield->name, "some_field") !== false && wire('modules')->SettingsModule->use_some_field == true) { $inputfield->label = "sample text"; $inputfield->collapsed = Inputfield::collapsedHidden; } }); }); I would have though that setting collapsed property before render would not allow the field to render. I want to handle some fields visibility in a settings module I use to manage enabling and disabling features in ProcessWire. An alternative strategy I do right now is that on the module save config I check for the module settings value and set the field to have it's input collapsed to hidden, but I'd like to think there is a more dynamic way at runtime? Does anybody have something like this working? -
Best way to move a Processwire site to a new server 2020?
da² replied to modifiedcontent's topic in General Support
@BIMAQ-Admin Take care that this properties in the destination site/config.php are the same as the original site: $config->userAuthSalt = 'xxx'; $config->tableSalt = 'xxx'; The procedure could be more simple, no need to install PW then delete the database. Just rsync the files between 2 servers (with some exclusions for sessions and cache), and use mysql_dump to copy database from old server to new one. This is how I replicate my dev environment to the staging server, except I also exclude site/config.php after it was initially uploaded and adapted to server config. Example of database copy from source server to destination, to run on source server: mysqldump --add-drop-database -uSQL_USER_SOURCE -pSQL_PASSWORD_SOURCE --databases DATABASE_NAME | ssh MY_SSH_USER@DESTINATION_SERVER_IP "mysql -uSQL_USER_DESTINATION -pSQL_PASSWORD_DESTINATION" rsync, to run on source server (BUILD_DIRECTORY is the main PW directory path, /var/www/SITE_NAME/html/ is the destination directory: rsync -avh --delete-delay --exclude-from=deploy-excludes.txt -e ssh BUILD_DIRECTORY SSH_USER@DESTINATION_SERVER_IP:/var/www/SITE_NAME/html/ deploy-excludes.txt: site/assets/cache/* site/assets/sessions/* I'm not 100 % sure about syntax for "cache/*", I use "site/assets/cache/" in my case. Idea in your case is to copy empty folder without content, in case PW doesn't create it at runtime. Note that I added a SSH key on destination server, it's why I don't need to specify SSH user and password in commands. -
Hello There fellow PW users. I am writing to get your advice on how to figure this problem out. I have never used the Front-end Editing functions of PW before so i tought i try it out. Using Ryans Front-end Page Editor module version 0.0.6. And i am using option A (automatic editing) method described in the documentation together with: <?PHP /* In my setup 'main_markdown_content' is a textarea field with a 'Markdown/Parsedown' textformatter. and inputfieldtype 'Textarea' selected on details tab for the field. Also content type is 'Unknown/Text' is selected. I get a warning when editing this field: -"This field currently allows Markup/HTML in formatted output. If you do not intend to allow Markup/HTML, please select the “HTML Entity Encoder” for “Text formatters”. But the field is storing Markdown and not HTML, as i understand it, the markdown gets rendered into HTML by the textformatter at runtime when outputting the field in my template ? */ /* using Option A (automatic editing) */ echo($page->main_markdown_content); ?> So far so good, it works, the trouble comes when the field is saved when clicking "Save" button on the frontend. For some reason new line endings are added to every line in my Markdown. Lets say this is the markdown (sorry for the swedish): # Sommar 2024 Sommar och sol, värme och regn om vart annat, för att inte tala om pollen. /EyeDentify. ## Uppdateringar - **2024-07-13:** Har uppdaterat Bulma.JS och önskar glad sommar. - **2024-04-10:** Önskar en trevlig vår. - **2023-12-24:** Önskar God Jul och Gott nytt år. - **2023-01-11:** Det är ett nytt år och jag fortsätter jobba på siten. Har planer för att utveckla den. Mer om det senare. And that after saving the frontend field ends up looking like this when i check it in the Admin editor: # Sommar 2024 Sommar och sol, värme och regn om vart annat, för att inte tala om pollen. /EyeDentify ## Uppdateringar - **2024-07-13:** Har uppdaterat Bulma.JS och önskar glad sommar. - **2024-04-10:** Önskar en trevlig vår. - **2023-12-24:** Önskar God Jul och Gott nytt år. - **2023-01-11:** Det är ett nytt år och jag fortsätter jobba på siten. Har planer för att utveckla den. Mer om det senare. (data above is copied exactly from the field after a save and it adds extra spaces.) Which is not what i want, the textformatter outputs the HTML as expected but it has more line endings and spaces between rows. I have to edit out the extra spaces in the Admin backend editor to make it go back to my original Markdown. I can´t figure out what is happening so i turned the editing of for now. Would realy like to be able to use it. You PW gurus have any ideas to what is going on ? If you want any other info to help me, just say so and i will try to supply it. Thanks again! /EyeDentify