Jump to content


  • Content Count

  • Joined

  • Last visited

  • Days Won


MoritzLost last won the day on December 12 2018

MoritzLost had the most liked content!

Community Reputation

140 Excellent

About MoritzLost

  • Rank
    Jr. Member

Contact Methods

  • Website URL

Profile Information

  • Gender
  • Location
    Cologne, Germany

Recent Profile Visitors

248 profile views
  1. Hi @Troost, I see a couple of possible reasons. For one, you are displaying multiple fields in the projectoverzicht.php, are those fields from the homepage or from the projects index page? The $page variable will always refer to the current page, so if the loaded page is the homepage, ProcessWire will look for those fields on the homepage. If the projectoverzicht.php is also used as a standalone-page (for example, if you have a page https://your-domain.de/projectoverzicht), then the $page variable will refer to this page instead, so that is a possible error. Besides that, you are using the $project_index variable from my example, but it isn't declared anywhere, that will throw a critical error in PHP. Make sure to load the index page first!
  2. If you post the template code that you have so far it will be easier to point you in the right direction. Anyway, in order to loop over the projects in your homepage template, you'll want to (1) get the projects index page, (2) find all children with the project template and (3) render the URL and preview image field in your markup. Something like this: // use the id of your actual projects page $project_index = $pages->get(156); // use the template name of your project template $projects = $project_index->children("template=project"); echo '<ul>'; foreach ($projects as $project) { echo '<li>'; echo '<a href="' . $project->url() . '">'; // use the field name of your preview image field echo '<img src="' . $project->preview_image->url() . '" alt="' . $project->preview_image->description() . '">'; echo '</a>'; echo '</li>'; } echo '</ul>';
  3. Personally, but I don't like to have my functions silently fail on invalid input. In this case, there's no useful thing the function can do if it doesn't receive a Pageimage, so you'd end up returning an empty string or null in this case (would need to make the return typehint nullable for the latter): function buildResponsiveImage(?Pageimage $img, int $standard_width): string { if ($img === null) { return ''; } /* ... */ } To me that feels like I'm creating a hard to debug error down the road, when I can't figure out why an image is not being displayed. Also, I want to be able to see all permutations of a given template by looking at it's source code; if an image field is optional, I want to see a conditional clause covering the case of an empty image field. Also, though I wrap the functions as static methods in a class, it's really more of a functional approach, so I'd rather create a higher-order function to wrap around this one and catch empty image fields in case I want to build on this. But this really comes down to personal preferences, and after all it was a tutorial on how to build such a function. I'd encourage everyone to build upon it and adjust it to their personal workflow / preferences, especially for things like error handling and default arguments
  4. @happywire There are two classes that ProcessWire uses for images: Pageimage and Pageimages (note the s). Each instance of Pageimage holds a single image, instances of Pageimages can hold multiple images (the class is basically an array wrapper around Pageimage objects). The function needs a single image, so you need to give it a single image. If your images fields contains multiple images (I suspect so because of the plural), you could for example loop through them and build a responsive image out of each of them, or just use the first one: // build a responsive image from each image in this field foreach ($page->images as $image) { $content .= buildResponsiveImage($image, 1200); } // build a responsive image from the first image in this field $content .= buildResponsiveImage($page->images->first(), 1200); You can also tell the API whether to return a Pageimage or Pageimages instance for this field by default. In the field settings for your images field, go to the Details tab; under formatted value, you can select Array of items to always return a Pageimages instance, or Single item to always return a Pageimage (this only works if your field is limited to one image). When in doubt, use get_class to find out what kind of object you're dealing with. Note you also have to check for an empty field, or the function will throw an error if there's no image in your field. // check object class echo get_class($page->images); // ProcessWire\Pageimages echo get_class($page->images->first()); // ProcessWire\Pageimage // make sure the field isn't empty before your pass the image into the function // for a Pageimage if ($page->images !== null) { $content .= buildResponsiveImage($page->images, 1200); } // for Pageimages if ($page->images->count() > 0) { $content .= buildResponsiveImage($page->images->first(), 1200); } Let me know if it doesn't work for you. Cheers Edit: Check out the documentation for the Pageimages and Pageimage classes.
  5. Thanks adrian! Yeah, I kinda overlooked the author field the first time, my bad ^^ Curious issue ... anyway, thanks for your help!
  6. Not sure where to post this, but I have a problem with the modules directory. I wanted to update the information for my module, but my changes won't appear. I thought they were in a manual review queue, but it's been a couple of weeks and nothing happened. Namely, I wanted to change those things in my Textformatter module: The stability from beta to stable. Add the category SEO/Accessibility. Anyone know why I can't update the information? Who is responsible for managing the modules directory, anyway? Only ryan or someone else as well? Would be nice if someone with access to that could take a look (Also, every time I go to the edit page, the "Author's Forum Name" field is empty, what's up with that? ^^)
  7. It doesn't work because your wildcard SSL certificate in the format "*.example.com" only matches one "level" of subdomain, so it's not valid for www.subdomain.example.com, see https://serverfault.com/questions/104160/wildcard-ssl-certificate-for-second-level-subdomain The redirect directives look good to me, Apache doesn't care about how many subdomains you're matching in the RewriteCond. But the browser cares about the invalid SSL certificate. The redirection won't work in this case, because the browser won't accept the SSL certificate, as it's not valid for www.subdomain.example.com. Since the SSL handshake and certificate validation takes places before the redirect headers are followed, the browser will show the security warning before doing anything else. I'd wager that if you manually add a security exception, the redirection will work. If you want to redirect https traffic to another https URL, you need a valid certificate for both the domain originally requested by the client and the target of the redirection, or the browser will show the security warning before or after the redirect, so your solution with the additional SSL certificate is the only one that can work. Why do you even need to support the second www subdomain? Sounds like one of "those" client requests
  8. Thanks for this @LostKobrakai! I just came across this very problem, thankfully I found this thread. Took me a while to understand how your solution works, certainly a creative solution to just add a string to make the cached value invalid JSON I modified your code slightly: const RAW_PREFIX = '::RAW::'; $wire->addHook('WireCache::saveRaw', function (HookEvent $event) { $args = $event->arguments(); $args[1] = RAW_PREFIX . $args[1]; return $event->return = call_user_func_array([$event->object, 'save'], $args); }); $wire->addHook('WireCache::getRaw', function (HookEvent $event) { $args = $event->arguments(); $cached_val = call_user_func_array([$event->object, 'get'], $args); return $event->return = $cached_val === null ? null : substr($cached_val, strlen(RAW_PREFIX)); }); I made two notable changes: If the Cache API returns null (i.e. no cached value exists), getRaw will also return null instead of an empty string. Use substr instead of str_replace, since it will be faster for long strings and it won't break anything in case the RAW_PREFIX (::HOOK::) appears anywhere inside the cached value (I'm a bit paranoid )
  9. I just pushed version 2.1.0 to Github & the modules directory! Includes a minimum length settings for linkable titles, a preference to link the oldest or most recent page if multiple pages have the same title, and an important bugfix for older MySQL versions. Check out the full changelog above
  10. Hey mdp, regarding your questions: The code you pasted will be called whenever any select element (or other input element, for that matter) with the class sort-select. This part is the key: $('.sort-select') That's a jQuery selector, the dot symbolizes a class selector (it mimics CSS selectors). If you want to do the same thing for an element with an id, use a pound sign instead instead: // select the element with id 'sort-select' $('#sort-select') // select any element that has either the class or the id 'sort-select' $('.sort-select, #sort-select') A cleaner, native way to handle this are GET-parameters in the url. First, you need to include a submit button in the form: <form id="sort-select" method="get"> <select name="sort"> <option value="-likes">Sort by: # Recommendations</option> <option selected value="title">Sort by: Title (A-Z)</option> <option value="-title">Sort by: Title (Z-A)</option> <!-- ... --> </select> <input type="submit" value="Submit"> </form> Just a side-note, your HTML attribute values should be encased by quotes. Also, the options need closing tags to be valid HTML5. I also removed the onchange attribute, though you can keep it in if you want to automatically trigger the submit. This is actually all that is needed! Since the form doesn't have an action attribute, it will go to the same page the form is on. The selected sort-option will be included in the targeted URL as a GET-parameter, where it can be picked up by PHP to generate the results listing. The onchange-attribute does the same thing, it basically auto-submits the form whenever you change the value of the select input (the code in the skyscraper.js file does the same thing, by the way). However, by including the submit-button, you make the entire form more accessible and ensure it will even work if JavaScript is disabled.
  11. Changelog Version 2.1.0 Feature: Prefer the oldest or the most recent page for automatic linking, in the case that two or more pages have the same title. Defaults to linking the oldest page. This is technically backwards-incompatible, as previously the selection of the linked page was pseudo-random in this case, but it's a pretty niche case. Feature: Set the minimum length for linkable pages. If you have pages with very short titles that appear commonly in different places, you might not want those linked every time. For example, if you set the minimum length to 5 characters, pages whose title is only four characters or shorter will never be linked. Tiny caveat: The length detection doesn't work correctly with emoji (or probably any 4-byte UTF8 character in the title). No idea why, the query uses CHAR_LENGTH which is supposed to accurately report multi-byte string length. If you know more about this, let me know! Bugfix: Version 2.0.0 changed the query in a way that would cause errors on older versions of MySQL, this is now fixed. Previous version might have thrown errors depending on the MySQL configuration (specifically, if ONLY_FULL_GROUP_BY was active), this should also not happen anymore. Miscellaneous I expanded the README, it now has a section about how to use the formatter manually. There are a couples of gotchas to this, so it could be helpful to beginners. The attribute settings are now parsed using preg_split instead of explode. This should prevent potential issues with windows-style CRLF linebreaks. Version 2.0.0 Backward incompatible changes Version 2.0.0 changes how titles are selected from the database and matched in text fields (see below for details). This is why this gets a major version number bump. The previous CSS classes setting has been removed in favour of the more general attributes setting (see below). Feature: Better HTML detection and edge case prevention The module got much better at checking the surroundings of a title to make sure it doesn't produce invalid HTML. In particular, it will now detect the following scenarios leave those as-is: If a page title is already inside anchor-tags. If a page title is inside attributes of other tags (for example, the description in a title-attribute. That said, the module still uses regex to find and replace titles, so it will never be able to cover all scenarios. For example, it will not detect if a page title is inside other tags that are not anchor tags (e.g. spans) which are in turn inside another anchor. So this module shouldn't be used on text fields where you need lots of custom HTML and intricate HTML structures. Feature: Configure arbitrary attributes to include in the created links You can now add attributes such as title, class, target, custom data-* attributes or anything you want to the links. This includes standalone attributes (without a value). For the attribute values, you can use replacement patterns that are passed to $page->getText(). For example, you can annotate the links with descriptive titles for accessibility or generate a class absed on the template of the linked page. Check out the examples on the module settings page! Feature: Longer titles get linked preferrentially If the title of one page is contained within the title of another page, the page with the longer title will get linked, not the shorter one. For example, if you have two pages titled "Content" and "Content Management System", the entire phrase "Content Management System" will be linked to the corresponding page. The page "Content" will not be linked in that instance. Of course, if in another part of the text you only have the word "Content", it will still be linked to the page as usual. This change was made possible by the detection of the edge cases mentioned above. Previously, the module would create the shorter links first to avoid creating nested links. The reason for this change is that longer titles tend to be more relevant for internal links. For example, in the previous case, the word "Content" could appear in many contexts not necessarily related to the page "Content", whereas the phrase "Content Management System" is much more specific. I may make this behaviour optional if there is a usecase for it. If you need this for some reason, let me know. Feature: Unicode support Now works with diacritics and emoji in the titles. To be fair, it was mostly working already, but now the regex includes the u Flag to explicitly turn off UTF-8 compatible functionality. There's still an edge case regarding diacritics (try creating two pages "Apfel" and "Äpfel" and you'll see it ...). I don't have a fix for it at this point, but it only occurs in very specific scenarios, so for now it'll do. Bug fixes Correctly detects multilanguage sites. Previously it would fail on a multilanguage site that used a non-multilangue title field. Adjusted the main database query to not produce warnings when fed directly to MySQL in certain configurations.
  12. @horst That looks interesting as well! Of course with pages it's easier to maintain, and clients can their own options. I usually only use pages for larger structures such as taxonomies, and Selectable Options for simpler display options, since it's a bit faster to set up. Maybe I'll try a "pure" setup with only pages for options next time ^^ Hm, this opens up some possibilities, something like creating setting groups or presets containing multiple other options .. I'll definitely play around with that one
  13. I don't think there's an inbuilt method for that, but you can quickly built your own! For the excerpt, you just need to find the first occurance of the search term inside the field you're searching, then display the text around that. A quick example: // get the search term from the GET-parameter and sanitize it $term = $sanitizer->text($input->get->q); // how many characters to include before and after the search term $include_around = 50; foreach ($pages->find('body~={$term}') as $result) { echo '<h2>' . $result->title . '</h2>'; // start and end positions of the search result in the body field $term_start = mb_strpos($result->body, $term); $term_end = $term_start + mb_strlen($term); // where to start and end the output, and additional // checks to make sure it's not out of bounds $offset_start = $term_start - $include_around; if ($offset_start < 0) $offset_start = 0; $offset_end = $term_end + $include_around; if ($offset_end > mb_strlen($result->body)) $offset_end = mb_strlen($result->body); $output = mb_substr($result->body, $offset_start, $term_start); $output .= '<mark>' . $result . '</mark>'; $output .= mb_substr($result->body, $term_end, $offset_end); echo '<p>&hellip; ' . $output . ' &hellip;</p>'; } I don't have a ProcessWire installation to test this right now, so might be some errors in there, but you get the idea ^^ Basically, find the term inside the field you're searching, then include some characters before and after it for context, and wrap the search term in <mark> tags for highlighting (you can also add a CSS class to target the search result for styling). You can of course refine this as much as you want, for example you can check for full stops to include complete sentences; you'll also want to make sure to find matches in the title as well, and in this case maybe only display the first X characters of the body ...
  14. Thanks ^^ I enjoy writing tutorials, I find it helps to consolidate the experience from my projects into organized knowledge. Most of the time I come across some areas where I'm not sure how something works, so I can look it up and further my own understanding (it's also great to improve my English as a non-native speaker). I think writing this stuff down helps bring out the core ideas or the important takeaways; before I started writing this one, I hadn't really considered the part about semantic/intuitive options, I mostly realized during writing that this was the crux of the matter. So yeah, I write for my own benefit as much as everyone else's
  15. This will be more of a quick tip, and maybe obvious to many of you, but it's a technique I found very useful when building display options. By display options I mean fields that control how parts of the page are displayed on the frontend, for example background colors, sizing, spacing and alignment of certain elements. I'll also touch on how to make those options intuitive and comfortable to use for clients. It basically involves setting up option values that can be used directly in CSS classes or as HTML elements and mapping those to CSS styling (which can be quickly generated in a couple of lines using a pre-processor such as SASS). Another important aspect is to keep the option values seperate from their corresponding labels; the former can be technical, the latter should be semantically meaningful. The field type that lends itself to this this seperation of concerns is the Selectable Options field, the following examples mostly use this field type. Note that this module is part of the ProcessWire core, but not installed by default. The following examples all come from real projects I built (though some are slightly modified to better demonstrate the principle). #1: Headline levels & semantics For a project that had many pages with long texts, I used a Repeater field to represent sections of text. Each section has a headline. Those sections may have a hierarchical order, so I used a Selectable Option field to allow setting the headline level for each section (you can guess where this is going). The definition of the options looks something like this (those are all in the format value|label, with each line representing one option, see the blogpost above for details): h2|Section headline h3|Sub-section headline Of course, the PHP code that generates the corresponding HTML can use those values : // "sections" is the repeater field foreach ($page->sections as $section) { // create an h2 / h3 tag depending on the selected option (called headline_level here) echo "<{$section->headline_level->value}>{$section->headline}</{$section->headline_level->value}>"; echo $section->body; } That's a pretty obvious example, but there are two important takeaways: I only used two options. Just because there are six levels of headlines in HTML, doesn't mean those are all relevant to the client. The less options there are, the easier it is to understand them, so only the options that are relevant should be provided. In this case, the client had provided detailed, structured documents containing his articles, so I could determine how many levels of hierarchy were actually needed. I also started at h2, since there should be only one h1 per page, so that became it's own field separate from the repeater. The two options have a label that is semantically relevant to the client. It's much easier for a client who doesn't know anything about HTML to understand the options "Section headline" and "Sub-section headline" than "h2" and "h3". Sure, it can be cleared up in the field description, but this way it goes from something that's quickly explained to something that needs no explanation at all. #2: Image width and SASS In the same project, there was also an image section; in our layout, some images spanned the entire width of the text body, others only half of it. So again, I created an options field: 50|Half width 100|Full width In this case, I expected the client to request different sizes at some point, so I wanted it to be extensible. Of course, the values could be used to generate inline styles, but that's not a very clean solution (since inline styled break the cascade, and it's not semantic as HTML should be). Instead, I used it to create a class (admittedly, this isn't strictly semantic as well): <img src="..." class="w-<?= $section->image_width->value ?>"> With pure CSS, the amount of code needed to write out those class definitions will increase linearly with the number of options. In SASS however, you only need a couple of lines: @each $width in (50, 100) { .w-#{$width}{ max-width: percentage($width/100); } } This way, if you ever need to add other options like 25% or 75%, you only need to add those numbers to the list in parenthesis and you're done. You can even put the definition of the list in a variable that's defined in a central variables.scss file. Something like this also exists in Bootstrap 4 as a utility, by the way. It also becomes easier to modifiy those all at once. For example, if you decide all images should be full-width on mobile, you only need to add that once, no need to throw around !important's or modify multiple CSS definitions (this is also where the inline styles approach would break down) : # _variables.scss $image-widths: (25, 50, 75, 100); $breakpoint-mobile: 576px; # _images.scss @import "variables"; @each $width in $image-widths { .w-#{$width}{ max-width: percentage($width/100); @media (max-width: $breakpoint-mobile) { max-width: 100%; } } } One important gotcha: It might be tempting to just use an integer field with allowed values between 10 - 100. In fact, the amount of SASS code would be identical with a @for-directive to loop throuh the numbers. But that's exactly what makes point-and-click page builders so terrible for clients: too many options. No client wants to manually set numerical values for size, position and margins for each and every element (looking at you, Visual Composer). In fact, having too many options makes it much harder to create a consistent layout. So in those cases, less is more. #3: Multiple options in one field Another example for repeatable page sections, this time for a two-column layout. The design included multiple variants regarding column-span and alignment. Using a 12-column grid, we needed a 6-6 split, a centered 5-5 split, a left-aligned 6-4 split and a right-aligned 4-6 split. I didn't want to litter the repeater items with options, so I decided to put both settings in one field (called something like Column layout) : center_6_6|6 / 6 (Centered) center_5_5|5 / 5 (Centered) left_6_4|6 / 4 (Left-aligned) right_4_6|4 / 6 (Right-aligned) As long as the value format is consistent, the individual options can be quickly extracted and applied in PHP: [$alignment, $width['left'], $width['right']] = explode('_', $section->column_layout->value); echo '<section class="row justify-content-' . $alignment . '">'; foreach (['left', 'right'] as $side) { echo '<div class="col-lg-' . $width[$side] . '">'; echo $section->get("body_{$side}"); echo '</div>'; } echo '</section>'; If you don't recognize the syntax in the first line, it's symmetric array destructuring, introduced in PHP 7.1. For older versions you can use list() instead. This example uses Bootstrap 4 grid classes and flexbox utility classes for alignment. The corresponding CSS can be quickly generated in SASS as well, check the Bootstrap source code for a pointer. Again, I'm limiting the options to what is actually needed, while keeping it extensible. With this format, I can easily add other column layouts without having to touch the code at all. #4: Sorting page elements A final example. In this case I was working on a template for reference projects that had three main content sections in the frontend: A project description, an image gallery and embedded videos (each using their own set of fields). The client requested an option to change the order in which those sections appeared on the page. Had I known this earlier, I maybe would have gone for a Repeater Matrix approach once again, but that would have required restructuring all the fields (and the corresponding code), so instead I used a Selectable Option field (labelled "Display order"). My approach is similar to the one from the last example: body_gallery_embeds|Description - Gallery - Videos body_embeds_gallery|Description - Videos - Gallery gallery_body_embeds|Gallery - Description - Videos gallery_embeds_body|Gallery - Videos - Description embeds_body_gallery|Videos - Description - Gallery embeds_gallery_body|Videos - Gallery - Description Since there are six possibilities to sort three items, this is the expected number of options. So I decided to include them all, even though some are probably never going to be used. I also tried to use a predictable order for the options (i.e. the options come in pairs, depending on what element is first). And here is the code used on the frontend: // render the template files for each section and store the result in an associative array $contents = [ 'body' => wireRenderFile('partials/_section-body.php', $page), 'gallery' => wireRenderFile('partials/_section-gallery.php', $page), 'embeds' => wireRenderFile('partials/_section-embeds.php', $page), ]; // e.g. 'gallery_body_embeds' => ['gallery', 'body', 'embeds']; $order = explode('_', $page->display_order->value); // echo the contents in the order defined by the option value foreach ($order as $item) { echo $contents[$item]; } You can see how it will be easy to add an additional section and integrate it into the existing solution. Though a fourth item would result in 4! = 24 possibilities to sort them, so at that point I'd talk to my client about which layouts they actually need Conclusion I always try to keep my code and the interfaces I create with ProcessWire extensible and intuitive. Those are a couple of solutions I came up with for projects at work. They are certainly not the only approach, and there is nothing super special about those examples, but I found that putting a little more effort into defining options with meaningful labels and using option values that I can use directly in my templates makes the result less verbose and more maintainable. Some or most of this tutorial may be immediately obvious to you, but if you made it this far, hopefully you got something out of it Feel free to share your own methods to create display options, or how you would've approached those problems differently. Thanks for reading!
  • Create New...