Jump to content

Leaderboard


Popular Content

Showing content with the highest reputation since 01/23/2020 in all areas

  1. 27 points
    We’ve got several updates on the dev branch this week, but I want to finish one more of them before bumping the version to 3.0.150, so will likely do that next week. The biggest change this week is the addition of a new core class called FieldsTableTools. This will be used internally by the $fields API variable for manipulating field table schema in the database, and its methods are accessed from $fields->tableTools(); Though this is one you probably won't be using from the API unless developing Fieldtype modules, so if you just want to know what the benefits are, feel free to skip to the last two paragraphs. Initially these methods are primarily focused on managing unique indexes for fields, though will expand to manage other schema manipulations as well. An example of its utility is also included in this week’s commits—our email fields (FieldtypeEmail) now have the ability to enforce uniqueness at the DB level. If you edit an email field (Setup > Fields > email) and click on the Advanced tab, there’s now a checkbox there to enforce unique values, ensuring that a given email address cannot appear more than once (i.e. not appear on more than one page). The same ability will be added to text and integer fields (FieldtypeText and FieldtypeInteger) as well, but we’re starting with the email field because it’s needed for an update hopefully wrapping up next week: the optional ability to login to the admin with your email address. Having a unique index on a DB column is actually a pretty simple thing, but as it turns out, it takes a lot of code to support the ability in an existing installation. That’s because we have it as something you can toggle on and off (on that Advanced tab), and when you toggle ON a unique index, there can’t be any duplicate values in the existing data, or the index will fail to apply. So there’s a lot of supporting code in FieldsTableTools to do things like detect and warn about duplicate values, delete empty values before adding the index, identify when the index is present without querying the table, reporting error conditions in a manner that understandable and recoverable, as well as the actual schema manipulations that add or remove the index. I realize this sounds a bit technical (and that's partly why I'm not putting this in a more formal blog post), but I think most developers will at some point find it very useful in building sites and applications. Not only will it enable us to safely support login-by-email in the admin (coming next), but it’ll be useful in any situation where you need to prevent a value from repeating. Whether that is in preventing double bookings for a date, location, seat, etc., or preventing any kind of redundancy like post titles, author names, product titles, phone numbers, or codes (UPC, ISBN, ASINs, EIN, SSN, etc.), this feature can come in handy. And supporting it at the DB level is a lot more solid than supporting it at the code level. Right now it’s just supported in email fields, but all of the logic has been delegated to the newly added class so that we can easily support it in any other fields (with text and integer fields coming next). Following that, the $fields->tableTools(); will also be gaining methods for modifying other types of field schema. For instance, rather than just supporting the INT column type for integer fields, wouldn't it be nice to go to Setup > Fields > [some integer field] and select both unsigned and signed integers of different types: TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT, for any new or existing integer field? MySQL also offers a similar array of size variations for other types that would be useful in our text and textarea fields, among others. So you'll be continuing to see ProcessWire offer more options for existing FIeldtypes in future core versions, enabling you to more efficiently store and manage content in PW. But of course we'll do it in a way that keeps it simple, ensuring that you don't have to think about these things unless/until you want or need them.
  2. 20 points
    ... and everything went silky-smooth. I finally got around to update a small portfolio site from PW 2.3 straight to latest dev. I spent roughly one hour, tested everything, and hey: no major hassles. I updated the few modules I used (and installed some new ones like Tracy or AoS), updated PHP as well, and that was more or less it. Imagine having such an un-touched WP, Typo3 or MODX site, and updating straight to the latest version... What a nightmare. That is - if they would actually still run that long 🙂 I actually expected it to go as smooth as it did, but I'm always surprised and in awe again when I do such a big update. Thank you ProcessWire.
  3. 19 points
    I'm currently building a Fieldtype/Inputfield for selecting date and time ranges (eg for events). There was quite some interest in this thread, so I thought I start a dedicated discussion... Background: I guess everybody of us knows the problem: You want to present events on your website and you start with a new template and a title and body field... For events you'll also need a date, so you add a datetime field called "eventdate". Maybe the event does not take place on a specific day but on multiple days, so you need a second field... Ok, you rename the first field to "date_from" and add a second field called "date_to". So far, so good. Then you want to list your events on the frontend. Simple thanks to the pw API, you might think. But you realize that it's not THAT simple... Which events take place on a specific day? What would the selector be? Yeah, it's not that complicated... it would be something like: $from = strtotime("2020-01-01"); $to = strtotime("2020-02-01"); $events = $pages->find("template=event, date_from<$to, date_to>$from"); Why? See this example, where the first vertical line represents the $to variable and the second is $from: The start of the event must be left of $to and the end must be right of $from 🙂 Ok, not that complicated... but wait... what if the date range of the event (or whatever) was not from 2020-01-18 to 2020-02-25 but from 18 TO 25 (backwards)? The selector would be wrong in that case. And did you realize the wrong operator in the selector? We used date_to>$from, which would mean that an event starting on 2020-01-01 would NOT be found! The correct selector would be >=$from. That's just an example of how many little problems can arise in those szenarios and you quickly realize that the more you get into it, the more complicated it gets... Next, you might want to have full day events. What to do? Adding a checkbox for that could be a solution, but at the latest now the problems really begin: If the checkbox is checked, the user should not input times, but only dates! That's not possible with the internal datetime field - or at least you would have to do quite some javascript coding. So you add 2 other fields: time_from and time_to. You configure your date fields to only hold the date portion of the timestamp and show the time inputfields only if the "fullday" checkbox is not checked. We now have 5 fields to handle a seemingly simple task of storing an event date. That's not only taking up a lot of space in the page editor, you'll also have to refactor all your selectors that you might already have had in place until now! Idea So the idea of this module is to make all that tedious task of adding fields, thinking about the correct selectors etc. obsolete and have one single field that takes care of it and makes it easy to query for events in a given timeframe. The GUI is Google-Calendar inspired (I'm acutally right now liking the detail that the second time input comes in front of the date input). I went ahead and just adopted that: Next steps I'm now starting to build the FINDING part of the module and I'm not sure what is the best way yet. Options I'm thinking of are: // timestamps $from = strtotime("2020-01-01"); $to = strtotime("2020-02-01")+1; // last second of january // option 1 $pages->find("template=event, eventdate.isInRange=$from|$to"); $pages->find("template=event, eventdate.isOnDay=$from"); $pages->find("template=event, eventdate.isInMonth=$from"); $pages->find("template=event, eventdate.isInYear=$from"); // option 2 $finder = $modules->get("RockDaterangeFinder"); $finder->findInRange("eventdate", $from, $to, "template=event"); $finder->findOnDay("eventdate", $from, "template=event"); ... I think option 1 is cleaner and easier to use and develop, so I'll continue with this option 🙂 Future As @gebeer already asked here I'm of course already thinking of how this could be extended to support recurring events (date ranges) in the future... I'm not sure how to do that yet, but I think it could make a lot of sense to build this feature into this module. I'm not sure if/how/when I can realease this module. I'm building it now for one project and want to see how it works first. Nevertheless I wanted to share the status with you to get some feedback and maybe also get your experiences in working with dates and times or maybe working with recurring events (or the abandoned recurme field). For recurring events the finding process would be a lot more complicated though, so there it might be better to use an approach similar to option 2 in the example above.
  4. 17 points
    This week we’ve got a couple of really useful API-side improvements to the core in 3.0.151, among other updates. First we’ll take a look at a new $config setting that lets you predefine image resize settings, enabling you to isolate them from code that outputs them, not unlike how you isolate CSS from HTML. Following that, we’ll introduce and show you how to use a handy addition to our static language translation functions. If you’ve ever come across the term “abandoned translation”, you’ll really like what this adds— https://processwire.com/blog/posts/pw-3.0.151/
  5. 17 points
    Last Saturday we started getting hit with heavy traffic at the processwire.com support forums, and it soon became a full blown DDOS frenzy. This post describes all the fun, how we got it back under control, and what we learned along the way— https://processwire.com/blog/posts/driving-around-a-ddos-attack/
  6. 15 points
    This module is (yet another) way for implementing a cookie management solution. Of course there are several other possibilities: - https://processwire.com/talk/topic/22920-klaro-cookie-consent-manager/ - https://github.com/webmanufaktur/CookieManagementBanner - https://github.com/johannesdachsel/cookiemonster - https://www.oiljs.org/ - ... and so on ... In this module you can configure which kind of cookie categories you want to manage: You can also enable the support for respecting the Do-Not-Track (DNT) header to don't annoy users, who already decided for all their browsing experience. Currently there are four possible cookie groups: - Necessary (always enabled) - Statistics - Marketing - External Media All groups can be renamed, so feel free to use other cookie group names. I just haven't found a way to implement a "repeater like" field as configurable module field ... When you want to load specific scripts ( like Google Analytics, Google Maps, ...) only after the user's content to this specific category of cookies, just use the following script syntax: <script type="optin" data-type="text/javascript" data-category="statistics" data-src="/path/to/your/statistic/script.js"></script> <script type="optin" data-type="text/javascript" data-category="marketing" data-src="/path/to/your/mareketing/script.js"></script> <script type="optin" data-type="text/javascript" data-category="external_media" data-src="/path/to/your/external-media/script.js"></script> <script type="optin" data-type="text/javascript" data-category="marketing">console.log("Inline scripts are also working!");</script> The type has to be "optin" to get recognized by PrivacyWire, the data-attributes are giving hints, how the script shall be loaded, if the data-category is within the cookie consents of the user. These scripts are loaded asynchronously after the user made the decision. If you want to give the users the possibility to change their consent, you can use the following Textformatter: [[privacywire-choose-cookies]] It's planned to add also other Textformatters to opt-out of specific cookie groups or delete the whole consent cookie. You can also add a custom link to output the banner again with a link / button with following class: <a href="#" class="privacywire-show-options">Show Cookie Options</a> <button class="privacywire-show-options">Show Cookie Options</button> This module is still in development, but we already use it on several production websites. You find it here: https://github.com/blaueQuelle/privacywire/tree/master Download: https://github.com/blaueQuelle/privacywire/archive/master.zip I would love to hear your feedback 🙂 Edit: Updated URLs to master tree of git repo
  7. 15 points
    This is an update about how it's been running blog sites using ProcessWire. I hope it's OK for me to post in this category even though I've already showcased my sites awhile back. I thought it would be helpful for people to get a feel for what it's like to use ProcessWire on an ongoing basis for blogging. Often people talk about the development of a site, but it's not quite as often that we hear about the ongoing running of a PW site and how the PW API influences that, which is what I'll cover here. As background, we at The GrayFly Group own and run the blogs goodkidsclothes.com and flipfall.com. The development of these PW sites has been covered in a showcase thread for GoodKidsClothes and another for FlipFall. Here are some of the unique experiences I've had running these two sites. "Running" covers everything from coding and making modifications to the templates, to writing our articles, to interacting with ad partners or with others seeking us out for something related to one of those sites. So this is a different experience from agencies who develop for others; we develop for ourselves. As background, the main traffic to the websites comes from organic search results. Income from sites is from affiliate marketing and advertisements. GoodKidsClothes PW experience: "to think it is to do it." For GoodKidsClothes.com, one of the things we noticed was that if we could think it, we could do it, thanks to the easy-to-use PW API. The need for a change Here is a concrete example of what I mean: we noticed that many people would enter the site on an older article (e.g. via a search result). However, we continually put out a lot of time-sensitive information, e.g. a style guide, a piece of news relating to a change in a children's clothing company, etc. I didn't want people to miss out on this, yet many were, because after reading their entry page, they'd leave. They had no idea (unless they clicked on the link to home page) that there was another article that could be of value to them. All too often, by the time people learned about that new article via search results, they'd be too late for the news to be relevant - in fact, it wouldn't even be the newest article anymore by that point. The solution So, using the PW API, we modified the article template so that if someone was reading any article that was not the most recent article, then at the end of what they're reading, they'd see a small section highlighting the most recent article. Here is a screenshot: As you can see above, our newest article is highlighted immediately below the article they're reading, unless of course they are already reading the newest article. In the case shown above, the newest article (recipe-related) did not happen to be time-sensitive, but in most cases that article would be time-sensitive, so that's why we made this change. To make the change we simply used the PW API to query what the latest article was and store its identity in a variable - those sorts of queries we set up in _init.php. Then we modified the article template such that if the current page was not the latest article, to include the featured box that you see above. Another need for a change You'll also notice links in boxes above and below where the featured article box is. These are ads (they blend OK right?!) These ads brought another problem to our attention: we'd put the ads blocks on all articles equally. However, in the case of the most recent article, often the most recent article would usually have a time-sensitive offer or some other call to action e.g. signing up for our newsletter (well, not in the case of the recipe article above, but in most cases the latest article would have something we prefer the reader to do). We didn't then want our readers to get distracted by the ads and either leave the site, or click on an ad and click away from the site, instead of doing whatever the call to action is. The solution Again using the ProcessWire API, we modified the "article" template so that there was conditional logic on the ads: if the current page is not the latest article, include the ad code (otherwise no ads). This mean no ads were seen on the most recent article, allowing for less distractions to the reader on time-sensitive articles and more likelihood of them following through on the call to action. Conclusion for GoodKidsClothes We were able to make all these changes within minutes of thinking of them! In-house, without a ton of knowledge of programming, thanks to the awesome ProcessWire API. We actually made all those changes live, i.e. going in there and making changes to the code of the site as its running live. Yes, we had backups of the entire site and we always first save a copy of the template file under a different name (usually prefixing it with OLD_ ) before modifying the live version. This is how helpful ProcessWire is. We can make changes that benefit our site and make them in-house as we think of them. If this was done under some other CMS, we would be unable to make those changes without either a) hiring a developer or b) training up in whatever the other CMS is to make the changes in-house. Either way, it would take considerably more time to do anything. So, despite not having a formal programming background, we now have a very "nimble" site that we can adapt as needed to changes that we desire, within minutes of thinking of the change we need, with only needing to know a little PHP, html, and CSS, just the very basics, and looking up the PW API. FlipFall PW experience: "the answer is yes." In the case of FlipFall, there have been times when a potential ad partner asks a question like "can you put different ads on different categories?" or other things. Sometimes they are questions I ask myself of the website "Can we do A/B testing of different ads; i.e. show a certain ad block 50% of the time totally randomly and another ad block the other 50% of the time?" "How about ads from this company some of the time and a different company other times?" The answer is always "yes." Coming from other CMS's (that I used but did not program with) I used to brace myself a bit if I saw an email that asked "Can you....?" but now thanks to ProcessWire I don't have that bracing reaction any more. So long as I can think of a way to do it (and so far I always have, thanks to the PW API), I can say "Yes we can." More to the point, I can actually say "Yes, we can make those changes in-house within [whatever brief timeframe I think it will be]" instead of having to be vague about timeframes because of needing a developer. So I no longer worry about "Can you ...?" questions because the answer is yes. Overall conclusions ProcessWire is a superb CMS for those who own and run a site. The PW API makes it easy to make changes to the look and functionality of the site as needed. Such modifications wouldn't easily be possible on alternative CMS's that are heavily "theme-based".
  8. 14 points
    In an ongoing effort to provide a sort of case study, and more info about this, I'll post random screens and info about the various features of this site. (1) Custom Dashboards The site uses a custom module that supports multiple dashboards. Any given dashboard is configurable and access controlled also. This is the main dashboard: (2) The admin editor pages take advantage of some great modules, namely RuntimeMarkup @kongondo, PageFieldInfo @Robin S, Field Descriptions Extended and more, There is also a new module not released yet called Admin Comments, which for this project got a lot of use. When dealing with a large and complex data collection as was the case with this project, the editors benefited from the ability to have the data auto-analyzed on each work so the "auto flags" field helped with that. The comments also allowed editors to post information, ideas and comments right into the page editor. The AdminComments module also provides the option for any posted comment to be emailed to the other team members (selectable), and the notification email (which is customizable) allows the recipient to click directly to the editor for that page. This saved incalculable hours of work, and enhanced communication during the project, across this large data set.
  9. 13 points
    I hope that you all have had a great week! I’ve got several commits to the core on the dev branch this week, with both improvements and fixes. I’m going to save the version bump to 3.0.150 till likely next week, when there should be more to write about. In addition to working on and supporting the core and modules here, I’ve been collaborating with Pete (forum admin) on a client project in ProcessWire. It’s keeping us both pretty busy, but I really value and enjoy the opportunity to develop sites in ProcessWire—it’s always a nice change of pace to develop something using ProcessWire, in addition to developing it. And it’s also a real pleasure to collaborate with Pete, he does amazing work. While working on this project, I’m still very much focused on core and module updates, but emphasizing smaller core updates like fixing issues and making incremental improvements to existing parts of the core (like in this week’s commits). Once we finish the first phase of this project (mid February), then I’ll be focusing on some larger updates and additions (and the related blog posts). Thanks have a great weekend!
  10. 12 points
    Plenty of posts on the forum relating to Content Security Policy (CSP) and how to integrate it with Processwire. It's not too hard to implement a decent htaccess CSP that will get you a solid B+ at Mozilla Observatory. If you're after A+ it's a little harder because of all the back-end stuff... until you realize it's surprisingly easy. After a lot of testing, the easiest way I found was to specify only what is needed in the htaccess and then add your required CSP as a meta in your page template. Plenty of people have suggested similar. Works very easily for back-end vs front-end, but gets complicated if you want front page editing. Luckily, a little php will preserve back-end and front page editing capabilities while allowing you to lock down the site for anyone not logged in. None of this is rocket science, but CSPs are a bit of a pain the rear, so the easier the better, I reckon 😉 The only CSP I'd suggest you include in your site htaccess is: Header set Content-Security-Policy "frame-ancestors 'self'" The reason for this is you can't set "frame-ancestors" via meta tags. In addition, you can only make your CSP more restrictive using meta tags, not less, so leaving the back-end free is a solid plan to avoid frustration. Then in your public front-facing page template/s, add your desired Content Security Policy as a meta tag. Please note: your CSP should be the first meta tag after your <head>. For example: <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Security-Policy" content="Your CSP goes here"> <!-- followed by whatever your normal meta tags are --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="format-detection" content="telephone=no"> If you haven't got Front Page Editing enabled, this works fine by itself. Just one extra step is needed to make sure you don't have to worry either way. The easiest way I found to allow both CSP and front page editing capabilities is the addition of a little php, according to whatever your needs are. Basically, if the user is a guest, throw in your CSP, if they're not do nothing. It's so simple I could have kicked myself when it finally dawned on me. I wish it had clicked for me earlier in my testing, but it didn't so I'm here to try to save some other person a little time. Example: <!DOCTYPE html> <html> <head> <?php if ($user->isGuest()): ?> <meta http-equiv="Content-Security-Policy" content="Your CSP goes here"> <?php endif; ?> <!-- followed by whatever your normal meta tags are --> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="format-detection" content="telephone=no"> If you want it a bit more involved then you can add additional tests and be as specific as you like about what pages should get which CSP. For example, the following is what I use to expand the scope of the CSP only for my "map" page: <?php $loadMap = $page->name === "map"; ?> <!DOCTYPE html> <html> <head> <?php if ($user->isGuest()): ?> <meta http-equiv="Content-Security-Policy" content="default-src 'none'; base-uri 'self'; manifest-src 'self'; form-action 'self'; font-src 'self' data: https://fonts.gstatic.com; frame-src 'self' https://www.youtube.com; img-src 'self' data:<?php echo ($loadMap) ? " https://maps.googleapis.com https://maps.gstatic.com" : ""; ?> https://www.google-analytics.com; script-src 'self' <?php echo ($loadMap) ? "https://maps.googleapis.com " : ""; ?>https://www.google-analytics.com https://www.googletagmanager.com; style-src 'self' <?php echo ($loadMap) ? "'unsafe-inline' https://fonts.googleapis.com" : ""; ?>"> <?php endif; ?> Hope this saves someone a little time testing. https://observatory.mozilla.org/analyze/bene.net.au
  11. 12 points
    I was tired of adding 6 different fields to an event-template just for storing the time range of the event... 😎
  12. 11 points
    Hi guys, I was very excited for this module, but my life took a huge direction change and I no longer have the time to invest in module development. I am gonna leave the files here. You guys can take it and run. Maybe there might be something useful here. Maybe not. I still think it's a good idea to do drag and drop modal building in PW. So hopefully one day something like that can come to light. I love this community and I love ProcessWire. Live long and prosper. - Joshua Designme 2.zip
  13. 11 points
    This is a new module that provides a simple solution to clearing all your cache layers at once, and an extensible interface to perform various cache-related actions. The simple motivation behind this module was that I was tired of manually clearing caches in several places after deploying a change on a live site. The basic purpose of this module is a simple Clear all caches link in the Setup menu which clears out all caches, no matter where they hide. You can customize what exactly the module does through it's configuration menu: Expire or delete all cache entries in the database, or selectively clear caches by namespace ($cache API) Clear the the template render cache. Clear out specific folders inside your site's cache directory (/site/assets/cache) Refresh version strings for static assets to bust client-side browser caches (this requires some setup, see the full documentation for details). This is the basic function of the module. However, you can also add different cache management action through the API and execute them through the module's interface. For this advanced usage, the module provides: An interface to see all available cache actions and execute them. A system log and logging output on the module page to see verify what the module is doing. A CacheControlTools class with utility functions to clear out different caches. An API to add cache actions, execute them programmatically and even modify the default action. Permission management, allowing you granular control over which user roles can execute which actions. The complete documentation can be found in the module's README. Beta release Note that I consider this a Beta release. Since the module is relatively aggressive in deleting some caches, I would advise you to install in on a test environment before using it on a live site. Let me know if you're getting any errors, have trouble using the module or if you have suggestions for improvement! In particular, can someone let me know if this module causes any problems with the ProCache module? I don't own or use it, so I can't check. As far as I can tell, ProCache uses a folder inside the cache directory to cache static pages, so my module should be able to clear the ProCache site cache as well, I'd appreciate it if someone can test that for me. Future plans If there is some interest in this, I plan to expand this to a more general cache management solution. I particular, I would like to add additional cache actions. Some ideas that came to mind: Warming up the template render cache for publicly accessible pages. Removing all active user sessions. Let me know if you have more suggestions! Links https://github.com/MoritzLost/ProcessCacheControl ProcessCacheControl in the Module directory
  14. 9 points
    Inputfield Selector Select ID Uses the Page List Select inputfield for user-friendly input of page IDs into Inputfield Selector. Overview This module adds a feature to Inputfield Selector, which is most commonly seen in Lister (Find) and Lister Pro. When adding a filter row for "Parent", "Has parent/ancestor" or "ID" the user is expected to enter a page ID to match against. But this is not as user-friendly as it could be because the user may be able to identify the desired page by its title or location in the tree but not know its ID. This is particularly the case for site editors who may not even understand the concept of a page ID. So this module adds a thunderbolt icon to relevant rows in Inputfield Selector. When the icon is clicked a Page List Select inputfield opens in a modal window, allowing the user to visually select a page from the tree. When the modal is closed with "OK" the ID of the selected page is inserted into the filter row. Tip After a page selection has been made in the modal window the "OK" button will automatically receive focus so if you prefer you can close the modal by hitting the Enter key rather than mousing to the OK button. Installation Install the Inputfield Selector Select ID module. https://github.com/Toutouwai/InputfieldSelectorSelectID https://modules.processwire.com/modules/inputfield-selector-select-id/
  15. 8 points
    Heya! Just wanted to drop a quick note here regarding this point 😊 I get why you feel that this is a problem and "an ivory-tower approach to things", but it's worth keeping in mind that with all design decision you'll get some benefits, but also some drawbacks. In systems where there's no "true" tree structure (some CMS products have gone this route) this would likely be an easy thing, but since ProcessWire is indeed hierarchical, it's going to need a bit more work. At the same time the structure in ProcessWire is predictable, efficient, and works great when a site consists of a variety of different content types. I've worked with other types of systems as well (including WP), and would choose PW's hierarchy any day over any of the alternatives. Regarding "switching the root", I've run into something similar once, and that's after being around PW for almost a decade and having built, maintained, and rebuilt quite a few ProcessWire sites in that time. What you're asking for may seem like a simple thing, but it's really not (given our context), and it's also quite a rare request — if it wasn't, it'd probably make sense to give it more consideration at core level 🙂 To give you a bit of context, this is roughly the same as a Linux user stating that "I just need to switch the system root, it's an easy thing right?". Well, you can do that as well, but it's really not an easy thing, and may in fact be very destructive (unless you know exactly what you're doing). Again, design decisions; you may disagree with them, but the fact that this issue rarely comes up tells me that this particular design decision was likely a good one. Just my five cents 🤷‍♂️
  16. 8 points
    I worked hard on the module the last days, and rewrote much of the core logic and added some more features. I wanted to record a screencast yesterday, but my recording software ignored my mic. So I show you some annotated screenshots instead. Since then, many things changed again. Right now I am working on downloading and installing a module via a URL, and also making a transition to modal dialogs instead of panels, if applicable. I moved the whole button logic from the PHP script to a vue component. So I am flexible how to display the buttons, and I can reuse them in the cards and in the table layout: I added a reduced card layout, which moves the description of a module into the "more information" accordion: Updated TODOs with much more features to come. I will work on the module in the next 2 days and hope to make so much progress, that I can finally release the first version on github.
  17. 8 points
    We've been expieriencing high load for some reason for the past 12+ hours though not sure where it's coming from as yet. As a result the site has been slow/showing error messages. The site and forums are up for now but please bear with us whilst we investigate.
  18. 8 points
    UPDATE 2020-01-30 SnipWire 0.8.0 (beta) first public release! It is done! After almost 1 year of hard work and a lot of help from the usual suspects I just released the first public version! Please note that this is still a beta so please test, test, test before you use this plugin in a real project. The module can be loaded via PW modules directory (just submitted - in review I think) or directly via GitHub. More infos will be added soon ...
  19. 8 points
    @Jens Martsch - dotnetic, @rjgamer, @teppo, @szabesz, @erikvanberkum, @dragan, @Lutz, @eydun, @psy, @horst, @cstevensjr, @tpr Just tagging in those who have reacted/responded to this topic in order to bring your attention to the notice above. Please don't uninstall but update to v0.2.1 or greater ASAP. Again, my apologies for the error. The intention was to have the module clean up after itself on uninstall but it's just too risky.
  20. 7 points
    Hi, With the complete deprecation of Instagram's API coming soon, I've been trying to get a replacement module developed to use the Instagram Basic Display API. I've got a proof-of-concept in place: https://github.com/chriswthomson/InstagramBasicDisplayApi At the moment, this module is very much alpha and shouldn't be used in production, but may assist you in getting your Facebook App tested if you are looking to use this API in future. Getting your Facebook App verified is another thing. I've not got to this stage yet, but having a verified Business Manager account attached should hopefully help once I do get to this stage. I hope to make this module a full implementation of this API, which also assists in leading the dev through the App setup process, however this is still a while off. We have used the soon to be deprecated InstagramFeed module on a number of large sites though, so we'll be hoping to get this up and running before March. Stay tuned, Chris
  21. 7 points
    Even though not officialy supported by PW3 I've used this module in the past successfully: https://processwire.com/talk/topic/711-release-schedulepages/
  22. 7 points
    Just a heads up, I hope this might save someone some time in the future. TL:DR> It's taken me hours to work out that Apache version 2.4 <If> statements are broken when trying to do regular expression matches against the %REQUEST_URI variable. Try matching against the variable called %THE_REQUEST instead - but be careful with your regex. Background: I've been trying to add more relaxed CSP headers for the admin portion of one of my PW sites because my default, strict, CSP headers stop some admin features from working properly. After diving in to Apache2 regexs and conditionals, I thought I had the answer with this... <If "%{REQUEST_URI} !~ m#^/admin/?#i"> # CSP headers for external visitors. Header set Content-Security-Policy "<strict policy for public site>" </If> <Else> # CSP headers for admin visitors. Header set Content-Security-Policy "<lax policy for admin>" </Else> ...but I couldn't make it work. After experimenting for several hours I found that the regex shown above does work if you limit it to a single-character match; which is essentially useless for determining if we are in the admin interface. However, you can achieve a useful match with the following... <If "%{THE_REQUEST} !~ m# /admin/?#i"> ... </If> <Else> ... </Else> ...which checks verses the entire HTTP request line made to the server. This block can go in your vhosts file or in the site's .htaccess file. Here's the Apache documentation: https://httpd.apache.org/docs/current/expr.html and it seems I'm not alone in having this issue: https://serverfault.com/questions/940953/apache-if-statement-not-working?noredirect=1&lq=1 although neither of these links had workable solutions for me.
  23. 6 points
    module is also now in the modules directory at https://modules.processwire.com/modules/modules-manager2/ Still pending
  24. 6 points
    You can't, without running into major headaches, change the root page (i.e. "Home"). What you can do is create a copy and move all children with the exception of the system fields (admin, trash, 404) under the new home. Then you can adapt the Home page to your liking (i.e. switch template, edit contents, whatever). Here's a small script that automates that task. This hasn't been tested extensively, so I don't advise to run this on a production system, only on a copy (or at the very least make a test run on a copy and then make sure you have a full, working database backup at hand). You can copy this code into a script (e.g. as "clonehome.php") in your PW installation root and run it from the command line. <?php namespace ProcessWire; if(PHP_SAPI != 'cli') exit; include('index.php'); error_reporting(E_ALL); ini_set("display_errors", true); /* ************************************************************** * CONFIGURATION FOR CLONING * *************************************************************/ $adminUser = "admin"; // PW Backend User $childrenToLeave = [ $config->adminRootPageID, $config->trashPageID, $config->http404PageID = 27 ]; /* ************************************************************** * END OF CONFIGURATION * *************************************************************/ $session->forceLogin($users->get($adminUser)); $home = $pages->get($config->rootPageID); echo "Cloning Home" . PHP_EOL; $newOldHome = clone $home; $newOldHome->setQuietly('_cloning', $home); $newOldHome->addStatus(Page::statusSystemOverride); $newOldHome->removeStatus(Page::statusSystem); $newOldHome->removeStatus(Page::statusSystemID); $newOldHome->id = 0; $newOldHome->setIsNew(true); $newOldHome->parent_id = $home->id; $newOldHome->of(false); $newOldHome->set('numChildren', 0); $newOldHome->removeStatus(Page::statusSystemOverride); $pages->save($newOldHome, ["quiet" => true]); echo "Home page cloned, new id is {$newOldHome->id}." . PHP_EOL; $childrenToLeave[] = $newOldHome->id; echo "Moving children:" . PHP_EOL; // Now move all children, with the exclusion of admin tree and special pages (404, Trash) // as configured in $childrenToLeave foreach($pages->find("parent=$home, include=all") as $child) { if(in_array($child->id, $childrenToLeave)) continue; echo "- {$child->name}..."; $child->of(true); $child->parent = $newOldHome; $child->save(); $child->of(true); echo "[X] Done" . PHP_EOL; } echo "FINISHED. All children moved." . PHP_EOL;
  25. 6 points
    I have found this library quite useful in the past. If you are going to support recurring dates, please take a look at the inputfield interface for recurme (it works quite nicely), but please make sure if you implement momentjs for any part of that process be sure to use the timezone version of it. I also think the ability to output recurring dates using RRULE (https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html) please be sure not to follow the broken format used by Recurme. I would recommend a full read of the recurme support thread as it raises lots of issues which should help you to avoid them in this module 🙂
  26. 6 points
    For 90% of these kinds of cases I do it like @Jens Martsch - dotnetic suggested, and in the template of the page (event, news item, etc) I have: // Throw 404 if item is future-dated if(!$user->isLoggedin() && $page->getUnformatted('date_1') > time()) throw new Wire404Exception(); But on a complex site where the pages might be queried from many selectors in many different places then unpublishing is the way to go.
  27. 6 points
    There might come many posts in the near future, sorry for that. I'll use this thread to let you follow and to have some kind of notes for writing docs when I release the module... It's also easier to quote and link to a specific post than to a section of one long post. The field now has 2 settings for hiding the checkboxes for toggline the fullday and has-end setting. This means you can use this field as a replacement for the datetime field and use the custom selector options shown above (range.onDay/inMonth/inYear).
  28. 6 points
    You go away for a minute and this is what happens...😀 TL;DR From Device-centric to App-centric development: ambient computing Dart and Flutter ranked #1 and #2 for fastest-growing language and open source project respectively over the last twelve months - GitHub’s 2019 State of the Octoverse report. Flutter is now one of the ten most starred software repos on GitHub. Flutter described as “the fastest-growing skill among software engineers”. LinkedIn, 2019. Flutter for web is in beta. Flutter for desktop is in alpha for mac-OS. You can edit Flutter code, run it and view the rendered UI online in DartPad. Adobe XD, Supernova, etc: flutter plugins. Full announcement here. Videos here (Flutter Interact 2019).
  29. 6 points
    I can't properly compare Svelte vs. Vue as I've zero experience from Svelte (and not a whole lot from Vue either), but regarding this question: it depends a lot on how complex stuff you're planning to build. I'm still using jQuery — or just vanilla JS, since native features have mostly caught up with jQuery by now — for stuff I can do almost completely with plain HTML and CSS (with a PHP backend), while for "complex" front-end stuff where a lot of data is processed, sorted, filtered, displayed (possibly in different ways), etc. I would definitely go with Vue. From what I have heard, the most common argument for Vue (vs. Svelte) seems to be that the Vue community is much more mature, and you'll find a lot more ready-made stuff for it. And, of course, more people with the same issues (which, as a relative newcomer to the JS scene myself, I've found really useful). This is all very opinionated, but the way I see it, jQuery is great if you just want to sprinkle HTML with some added-on behaviour, while for bigger stuff it actually tends to get more complex than if you'd chosen a full-blown front-end frameworks in the first place. So again, it depends on what you're actually building 🙂 If you do decide to go with Vue, for admin UI's in particular you might want to check out https://vuetifyjs.com. With Vuetify you'll have to sacrifice some freedom in terms of UI design, but once you get up to speed with it, it's pretty amazing — in many cases seemingly complex stuff gets done in a matter of minutes 😅👌
  30. 6 points
    Hi @adrian, I didn't know that module existed, it's not in the directory, is it? Because I looked if something like this already existed and didn't find anything. Might've saved me some trouble 😅 That said, I had a quick look, here's what I found in comparing both modules: ClearCacheAdmin exposes more options through the setup page, so there are available to everyone, whereas ProcessCacheControl has it's options in the module configuration, so they are only available to the developer. ClearCacheAdmin has multiple links (and multiple options) on the process page, giving the user more fine control, whereas ProcessCacheControl goes for a simpler interface and bundles all those actions into one (configurable) action with a single button. ProcessCacheControl can delete $cache entries without an expiry date, which ClearCacheAdmin doesn't as far as I can tell. ProcessCacheControl also lets you configure caches to be deleted by namespace, whereas ClearCacheAdmin offers each cache entry to be deleted individually (I think this has mostly to do with the module being a bit older, I believe $cache didn't have all those options back then). The largest difference is my concept of "cache actions", which ClearCacheAdmin doesn't have. I'm not sure how useful that will actually be yet. I think if I can expand on the actions available by default, it will be pretty handy. With ProcessCacheControl, you can add custom cache actions / profiles through the API, that may be useful depending on the use case. Adding to that, ProcessCacheControl has permission checks on a per-action basis. ProcessCacheControl can be modified and executed through the API. In particular, you can modify the default action and execute any action programmatically.
  31. 6 points
    I wanted to write a bit of an update since this project was so well received last year. The Architekturführer has now been online for about nine months, and we have been getting mostly positive feedback. Thankfully, I was able to resolve all issues with Google's indexing of the page. The site's SEO is actually pretty solid now, being one of the first results when searching for "Architekturführer" (at least from within Cologne). We have also added some new content and restructured some pages. The homepage, for example, now features only selected projects. An iconic new entry that everyone who's been to Cologne in recent years will recognize is the Rheinboulevard Deutz. There's also an entirely new section for architecture-themed walks (Spaziergänge) that you can take in different areas of the city. If you come to visit for a day, make sure to check those out 🙂 Finally, we got some traffic through word of mouth and a couple of architecture news sites, the visitor numbers are steadily rising each month. The only thing I find a bit lacking is that the number of PWA installations are very low. It seems the whole PWA concept has not really come through yet, or maybe it's just not that much of an added benefit compard to the normal site. I'm interested to see how PWAs will fare against native apps in the future, once they become more widely adopted.
  32. 6 points
    Heya! Just released Wireframe 0.9.0. Here's the changelog for this version: ### Added - New EventListenerTrait. Currently used by Components only. Adds support for listening to and emitting events. - Support for Renderer modules for adding templating engine support for view files, component view files, etc. - New Page methods Page::viewTemplate(), Page::getViewTemplate(), and Page::setViewTemplate(). - New method Component::getData() for manually defining the data passed to the component view. ### Changed - Controller::init() and Controller::ready() are now hookable methods. - Component::setView() and Component::getView() are now final methods, preventing accidental overrides. - Layout file is no longer necessary; if it's missing, the page can be rendered using just a view file. The component system is a bit more mature now, and there are a couple of new features: Component::on('event-name', callable $callback) can be used to listen to events emitted by a Component instance, and in a Component class events can be emitted with $this->emit('event-name', array $args). This is mostly just syntactic sugar on top of the hook system, but it seemed fun and potentially useful, so... 😅 The Component base class extends WireData, so by default what gets sent to the component view when a component is rendered is the internal $data array. Since 0.9.0 it's possible to alter this behaviour by implementing getData() method which returns the data that the component should expose to its view. Another "big" update is support for renderers. These are add-on modules that add support for different rendering / templating engines to Wireframe, and the first example is the Wireframe Renderer Twig module. I haven't really used a templating engine in a while so it's possible that I've missed something important, but in my limited tests the Twig engine seemed to work quite well right out of the box. I'll likely add a separate module for Latte at some point, just to make sure that the logic works for other engines as well 🙂 By the way, it looks like I forgot to post here about Wireframe 0.8.0. It was released last month, and here's the related changelog entry: ### Added - Support for Components, along with a new static factory method Wireframe::component($component_name, $args). - Support for rendering pages that have not been "routed" to Wireframe using the altFilename template setting. - New static getter/factory/utility method Wireframe::page($source, $args). - New static utility method Wireframe::isInitialized(). ### Changed - Wireframe::$initialized is now a static property. This was a necessary change so that Wireframe::isInitialized() could be implemented effectively. On a related note I'm considering tagging the next release as 1.0.0. We've been using Wireframe on production sites for a while now, and I can't recall any truly breaking changes so far, so I think it's already quite stable 🙂
  33. 6 points
    I'll join the "bliss train" have also updated sites running for years on old versions with no issues, I hope this thread gets mentioned in the ProcessWire weekly haha, people really need to know it's possible to live without waiting for your the site to break.
  34. 6 points
    Same here, i've been repeatedly graced with effortless and smooth upgrades countless times... such a godsend!
  35. 5 points
    I usually supplied "translation keys" instead of the default translation to translation functions, and translated them in all languages, eg. __('btn_submit') instead of __('Submit'). This way they rarely needed to be changed, only when the translation changed to something completely different.
  36. 5 points
    Thanks everyone for the feedback! I just pushed a new version that resolves a bug that prevented installs on PHP versions <7.4. PHP 7.1, 7.2 and 7.3 should work now! Thanks @Mikie for your help! I have also found some documentation in the ProCache store page. I've added an option to clear the ProCache now as well, though I haven't included it in the new version because I can't test it out. Would you mind giving it a go? The ProCache integration is in the procache branch on Github (download link). If it's working as intended, it should add a new option to clear the ProCache in the module configuration, including a note saying whether ProCache is installed. If selected, the 'Clear all' action should use $procache->clearAll and log the result. It would also be good to know if non-superusers can use this, or if it's limited to the superuser. Thanks! The module also appears to have been approved to the module directory, so it can now be downloaded directly through the backend using the module name ProcessCacheControl. Thanks @adrian (or is Ryan in charge of the module directory? 🙃)
  37. 5 points
    Right now only the default values can be overwritten via language translation files. But good hint - I'll add multi-language support for the config fields as well.
  38. 5 points
    Hi Bernhard, Being able to enter and display Events (or the like) is such a valuable tool for many websites. I am thrilled you have started this project. I cannot over emphasize @adrian's comment on all fronts. A few things that are very helpful in displaying recurring events is the ability to only display the event once. Ie. if you have a recurring event from March 10-15th. You may not want the event to display 6 times if you are viewing the Event list page on March 10th. The recurme module does do quite a few things right, unfortunately it wasn't quite there. Two features standout: 1. The user interface to create recurring dates which also has the ability to remove select dates. Ie. A range from March 10-15, but you can then exclude any date in the range. This avoids a second (essentially duplicate) event needing to be created. 2. Displaying recurring events can be quite a challenge, especially when there is a long list of dates. Recurme used a calendar display for this, which could really display a lot of recurring dates in a small space. Rather than displaying a long list dates in text format. I will send you a pm with some live examples we have built to date and how we display events. Perhaps this will help highlight some of the challenges you may be facing or how people may use your module. This will be such a great addition to the Processwire ecosystem!
  39. 5 points
    That works as well! Main reasons I don't (practically) ever use this approach have already been mentioned here: I'd have to block access on template level (may need to do that on a lot of places), search features and highlight lists etc. all have to be aware of this, and so on and so forth. Potentially having multiple developers work on the site doesn't really help either. At some point you're very likely to show publicly something you didn't intend to. Want to schedule pages? install SchedulePages. Need more scheduled content? Add two fields to the template. Easy as it gets 😋 Also, as a general rule if thumb, I try to do as little permission/access checking myself as possible. Sure, in this case it's probably not a big deal, but... 😁 "ProcessWire — there's more than one way to do it" ✌
  40. 5 points
    To be fair publishing (or un-publishing) pages at set time vs. just listing pages based on a datetime field are two different use cases. In some cases the distinction may not matter, but in others it will — i.e. when something really shouldn't be viewable before a predefined date/time 🙂
  41. 5 points
    @nabo What's your use-case? From a security point of view it's not a good idea to allow everything. Surely you have an idea what kind of file-types will be used? I found another forum thread where basically the same question was asked, and the gist of it was: No, you can't. Leaving the allowed extension config list empty makes it unusable, and just entering a wildcard * won't work either. @ryan takes security matters very seriously, and personally, I wouldn't want to have such a "everything goes" option built-in the core, too. If you have to, for some reason, import data from another system, and create / populate fields via API (batch actions), you can temporarily allow each file extension that comes your way, and then switch back to a "normal" default set after save. But I guess you were asking about using the GUI options.
  42. 5 points
    Update: Blog 2.4.5 Changelog Added new option (see example use below) to MarkupBlog::getArchives() that allows to specify if archive months should be sorted descending (Dec - Jan). Default is ascending (Jan - Dec), thanks to question by @montero4 Fixed bug in template blog-archives.php that caused illegal offset warnings. Module has been updated in the modules directory. Example usage of new getArchives() option getArchives() now accepts a third parameter as an $options array. If you want archive months to be sorted and rendered in descending order (December - January), you will need to pass this as an option in the $options array and set the value 'descending' to they key 'archives_month_sort_order'. Since this is the third parameter for the method, you will need to pass options for the first and second parameters as well. If you want the default ascending order of months, you don't have to change anything in your code. Just call getArchives() as usual without any parameters. Otherwise, read on for descending order of months. <?php $blog = $modules->get("MarkupBlog"); // options: if we want to sort archive months in descending order $archiveOptions = array('archives_month_sort_order'=>'descending'); // get archives: order months descending $archives = $blog->getArchives(0, 1, $archiveOptions); // if you want to render the archives $content = $blog->renderArchives($archives);// order months descending Please test and let me know. Thanks.
  43. 5 points
    Definitely an interesting topic! We've developed a few PWA's for our clients recently, and I'd say that they've been very well received — but, to be fair, they've been a) services for existing members and b) basically apps that wouldn't work (well) as regular websites, so that option was out of the question. When it comes to native vs. PWA, in our case PWA seems like the obvious choice: easy to use and efficient to maintain, upgrades are effortless, and obviously the web platform is "our thing" (more than native anyway) 🙂 Some "random" websites (news sites, blogs, etc.) are now offering the option to install, but to me that feels a bit weird: unless it's a service I'm going to use regularly and there's a clear benefit for me in installing it, I don't really see the point. In fact it can also be a little intimidating: why do I need to install this service to use it? Again I think it boils down to the question of "would it work as a regular website": if the answer is "yes", then perhaps it should just be that 🤷‍♂️ (Sorry to hijack the thread, by the way!)
  44. 5 points
    Opened an issue for this here: https://github.com/processwire/processwire-issues/issues/1083. It's great that http to https redirects are finally in place (assuming that this was intentional), but it appears that it's going to be an issue for the core.
  45. 5 points
    You want to write that line: $mobile_menu = $homepage->children; in one of a few different ways, e.g.: $mobile_menu->add($homepage->children); // or, though no need to create a new PageArray beforehand in this case: $mobile_menu = $homepage->children->makeCopy(); to avoid any caching issues and avoid changing "builtin" PageArrays. If you directly assign $homepage->children, $mobile_menu points to the already loaded instance of the children PageArray of $homepage and your new, empty PageArray gets discarded. Repeated calls to $pages->get(1) are cached and return the page object already in memory. I can only guess since you didn't post that part of the code, but it looks like you may have inadvertently manipulated $homepage->children when you built your main navigation. Make sure to use add() or makeCopy() in that part too.
  46. 5 points
    Here's a small new module that adds a "Manage tags" button to the template list, just like the field list already has. Easily add, remove and change tags for your templates. https://github.com/BitPoet/TemplateTagsEditList
  47. 5 points
    I just submitted my first module to the PW modules directory. The module is a full featured e-commerce module but I couldn't find a corresponding category there. May I suggest that a responsible person add a new category "e-commerce" (or something similar)?
  48. 5 points
    ...or you can use this small module that (with a lot of nicked code from ProcessField) adds the functionality to the Template list: https://github.com/BitPoet/TemplateTagsEditList Best try it out on a copy of the site though, I haven't tested it much yet. Anyway...have fun 😉 (I'm soon going to use this a lot myself to clean up a few really complex sites...)
  49. 5 points
    During a recent maintenance routine we found that our website's database (1,700+ pages) had thousands of instances of unnecessary, garbage code that had come with copied text from Word. Passages with margins expressed in points, cms and inches, and some that were wrapped in upwards of 7 spans were among the most easily identified crimes. Purging all of this dropped our database size by over 4%. A few of the code examples above nuke all inline styles, which will impact some important out-of-the-box functionality for PW3 and CkEditor (depending on your use); specifically with many of the options with tables and lists, such as setting a column width or changing the bullet styles within a nested list. To work around that, I made some changes to Ryan's code to target specific tags and to eliminate spans (which you can only add via Source view without pasting them in). $wire->addHookAfter('InputfieldCKEditor::processInput', function($event) { $inputfield = $event->object; $value = $inputfield->attr('value'); if(strpos($value, 'style=') === false) return; $count = 0; $qty = 0; // Optional remove spans $value = preg_replace('/<span.*?>/i', '', $value, -1, $qty); $value = preg_replace('/<\/span.*?>/i', '', $value, -1); $count = $count + $qty; // Remove inline styles from specified tags $tags = array('p','h2','h3','h4','li'); foreach ($tags as $tag){ $value = preg_replace('/(<'.$tag.'[^>]*) style=("[^"]+"|\'[^\']+\')([^>]*>)/i', '$1$3', $value, -1, $qty); $count = $count + $qty; } if(!$count) return; $inputfield->attr('value', $value); $inputfield->trackChange('value'); $inputfield->warning("Stripped $count style attribute(s) from field $inputfield->name"); });
  50. 5 points
    I understand why you are asking for this feature. You're not the first one. But it's not as obvious as it seems. The 'title' field is not the same as all other built-in fields. You can't modify the behavior of the built-in fields per template basis. Which means they all behave the same no matter what template the page has. That's why we have a generic type 'Page' that has all the built-in fields. No matter what template the page we can confidently serve those fields for every page. The 'title' field's behavior could change depending on the template. I understand that it is almost never the case, but semantically it is. For example, you can set different access settings for 'title' field depending on the template. You make it that user can view the 'title' on one template and not on the other. You can change the description of the field for each template and it will appear in the GraphQL documentation. You can also make 'title' field 'not required' for one template and 'required' for others. So, including 'title' field into the Page type will break the semantics. I understand that 'title' field is almost always treated as a built-in field but I just can't overcome this feeling that it is the 'wrong' way to do it. I would like do it only the 'right' way. And the right way brings us to your second question. If we implement it properly and add the ability to get the values of the template fields for generic Page types, then 'title' field should also be solved. For this we will be adding interfaces. It will allow you to get template fields for Page types by providing a template. It will look something like this. { city{ list{ children{ # <== let's say children are the pages with template skyscraper and architect list{ id name ... on SkyscraperPage { # <== you basically say: "for skyscraper pages give me these fields" title images{ url width height } } ... on ArchitectPage { # <== "and for architect pages give me these fields" born email resume{ url filesize } } } } } } } this way you can fetch values for all template fields on Page types. This will work with everything that returns a Page type. Including 'child', 'children', 'parent', 'parents' and Page Reference fields too. And the best part is, it will be semantically correct! 😄
×
×
  • Create New...