Since I started in 2002, all iterations of portalZINE have been pure english content websites. You can read about the why on my services page.
I had potential customers in Germany complain about that a lot over the past few years. But your own website often suffers, while your customers get all the attention. That is how it is and how it should be!
Creating Multi – Language websites has been part of my services & portfolio for years, with an extreme application setup handling 13 languages in 2014 for the soccer world cup.
Multi-Language setups have come a long way and it was time to showcase that on my own setup as well. Not only to calm those potential customers, but to testdrive new functionality and possibilities on my own setup. portalZINE has always been my testlab for stability and new feature sets.
Most of my static pages are available in English and German now, the blog itself will remain pure English.
Need help setting up a multi language website, get in touch!
Cheers
Alex
Updated 25.03. : Some function names changed in the latest beta version.
ACF 5.8 Beta introduced an easy way to create your custom Gutenberg blocks. I am already using it heavily for a current project, to easily organize content and media assets.
Really powerful, when combined with Timber as well, which has been the foundation of many of my themes for years now ;)
Organizing data using ACF is nice, but sometimes you seek access to that saved block data directly. I hate it when I am confined to boundaries and the data flow is restricted or hidden. I need things to be accessible to choose the creative flow myself.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// Parse blocks from post content $blocks = parse_blocks($your_post_content); $collect = array(); // Loop through the blocks foreach($blocks as $block){ //Setup global block post data context // before: acf_setup_postdata acf_setup_meta( $block['attrs']['data'], $block['attrs']['id'], true ); // Get ACF fields $fields = get_fields(); // I am using this to organize my assets. // Each block of mine has a unique identifier as its first field: // $uid = $block['attrs']['data'][array_keys($block['attrs']['data'])[0]] // I would do: // $collect[$uid] = $fields; // Collection of fields using the block id. $collect[$block['attrs']['id']] = $fields; // Restore global context // before: acf_reset_postdata acf_reset_meta( $block['attrs']['id'] ); } |
There you go, enjoy some free block data :)
I was a big skeptic, when it comes to WordPress and the new Gutenberg editor, but combined with ACF + Timber its pure magic :) Looking forward to things to come!
Cheers
Alex
Extended example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
$collect = []; $blocks = parse_blocks($your->post_content); foreach($blocks as $block){ if( isset($block['attrs']['data']) && !empty($block['attrs']['data'][array_keys($block['attrs']['data'])[0]])){ acf_setup_meta( $block['attrs']['data'], $block['attrs']['id'], true ); $fields = get_fields(); acf_reset_meta( $block['attrs']['id'] ); $collect[$block['attrs']['data'][array_keys($block['attrs']['data'])[0]]] = array('render' => render_block( $block ), 'field' => $fields, 'block' => $block ); }else{ $collect['main'] .= render_block( $block ); } } |
The $collect array will hold all data, including all ACF fields. You will have full access to any field, including repeater fields. The $collect[‘main’] will just collect the standard post content.
WooCommerce Appointments is a commercial booking plugin that allows you to setup appointments with WooCommerce. It has full integration into Google calendar to track appointments of your staff.
Staff availability can be set globally or via each staff members profile. While this is nice, I was looking for an option to actually handle availability via another Google Calendar as well. That was a must have feature for a current project.
What do you do, if that feature is not available yet ? You poke the code!
The magic entry point for the staff availability is the user meta “_wc_appointment_availability“, which is made available through includes/class-wc-product-appointment-staff.php.
A couple of weeks ago I asked support for a simple filter hook to alter the availability on demand from the outside.
The development team added the feature in one of the latest releases, making wc_appointments_staff_availability the entry point for my custom availability changes.
1 2 3 4 5 6 |
add_filter( 'wc_appointments_staff_availability', 'availability_callback', 10, 3 ); function availability_callback($availability, $staff){ // Your changes here return $availability; } |
You can either pull Google Calendar Events directly through the Google Calendar API or use the available iCal export option. In this quick example I will use the private calendar iCal export file.
Lets setup a quick clean calendar, called “Availability”. So simple and catchy :)
For this example I am using the PHP ICS Parser, but any other parser will do. Install it via composer: composer require johngrogg/ics-parser.
Lets create a quick little plugin to get us going and save it to /wp-content/plugins/CustomAvailability/smile.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
<?php use ICal\ICal; /* Plugin Name: WooCommerce Appointments - Custom Availability Plugin URI: https://portalzine.de Description: Attach additional Google Calendar for staff availability Version: 1.0 Author: portalZINE NMN Author URI: https://portalzine.de */ require_once(PATH_TO_VENDOR_DIR."vendor/autoload.php"); class StaffCustomAvailability{ function __construct() { add_filter( 'wc_appointments_staff_availability', array($this, 'staffAvailability'), 10, 3 ); } function staffAvailability($availability, $staff){ // Your changes here return $availability; } } $StaffCustomAvailability = new StaffCustomAvailability(); |
Its time to get the data into the system. I am only pulling and altering the availability for one single user in this example, the user with the USERID “3”. This should provide you with a good starting point.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
function staffAvailability($availability, $staff){ // $staff holds the complete user data // $availability is the current set of rules // Only altering availability for USERID 3 if($staff->data->ID == 3){ $newSet = array(); try { // add the private ics file here $ical = new ICal('URL TO PRIVATE ICS FILE', array( 'defaultSpan' => 2, // Default value 'defaultTimeZone' => 'Europe/Berlin', 'defaultWeekStart' => 'MO', // Default value 'disableCharacterReplacement' => false, // Default value 'skipRecurrence' => false, // Default value 'useTimeZoneWithRRules' => false, // Default value )); } catch (\Exception $e) { die($e); } $forceTimeZone = true; $events = $ical->sortEventsWithOrder($ical->events()); // looping through all events foreach ($events as $event) { // Get Start and end date / time information $dtstart = $ical->iCalDateToDateTime($event->dtstart_array[3], $forceTimeZone); $dtend = $ical->iCalDateToDateTime($event->dtend_array[3], $forceTimeZone); // Define new time:range rule // Adding one rule per day // Added Friday to the calendar + event recurring 4 times, which results in 4 new rules for staff member 3 $newSet[] = array( 'type' => "time:range",// rule type used for this example 'appointable' => "yes", '[priority' => 10, 'from' => $dtstart->format('H:i'), // start time 10:00 'to' => $dtend->format('H:i'), // end time 16:00 'from_date' => $dtstart->format('Y-m-d'), // start date - Friday 'to_date' => $dtend->format('Y-m-d') // end date - Friday ); $availability = $newSet; } } return $availability; } |
The example pulls and parses the ics file on every load, use a transient or REDIS to store data and only refresh in certain intervals.
Hope this gets you started! I build a simple interface around it, with a lot of more rule options. This makes the setup for each staff member a brise. Now each of them can setup a calendar easily and provide me with the ics link :) WooCommerce Appointments rocks …
Simple little snippet, that can easily be used in conjunction with wp_update_user()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
/* Generate Unique User Nicename */ function generate_unique_user_nicename( $x ) { // set up args to query $args = array( 'search' => sanitize_title( $x ), 'search_columns' => array( 'user_nicename' ) ); // query for user $user_url_exists = new WP_User_Query( $args ); $results = $user_url_exists->get_results(); // if user url is found, check for new one if( !empty( $results ) ) { $i = substr( sanitize_title( $x ), intval( strrpos( sanitize_title( $x ) ,'-' ) ) + 1 ); if( is_numeric( $i ) ) { $i++; $y = substr($x, 0, strrpos($x,'-' )); $company_name = $y . '-' . $i; } else { $company_name = $x . '-1'; } $new_company_name = sanitize_title( $company_name ); return generate_unique_user_nicename( $new_company_name ); } else { return sanitize_title( $x ); } } |
When using your main content feed to share posts via buffer or other services, it is crucial that your feed validates cleanly.
There are always things in the generated WordPress content that can break your feed validation. Here some things to cleanup or alter your delivered feed content.
is_feed()
Check for syndication request. This tag is not typically used by users; it is used internally by WordPress and is available for Plugin Developers.
Check for feed syndication in your themes functions.php
1 2 3 |
if (is_feed()) { /.. tweaks go here ../ } |
1 2 3 4 5 6 7 8 9 |
function rss_noiframe($content) { $content = preg_replace( '/<iframe(.*)\/iframe>/is', '', $content ); return $content; } add_filter('the_excerpt_rss', 'rss_noiframe'); add_filter('the_content_feed', 'rss_noiframe'); |
1 2 3 4 5 6 7 8 9 10 |
function rss_nocomments($content) { global $post; $post->comment_status="closed"; } add_filter('the_excerpt_rss', 'rss_nocomments'); add_filter('the_content_feed', 'rss_nocomments'); |
The sizes attribute breaks feed validation, here how to clean it up.
1 2 3 4 5 6 7 8 9 |
function no_responsive_image_feeds() { add_filter( 'max_srcset_image_width', function() { return 1; } ); } add_action('rss2_head', 'no_responsive_image_feeds' ); add_action('atom_head', 'no_responsive_image_feeds' ); add_action('rss_head', 'no_responsive_image_feeds' ); |
1 2 3 4 5 6 7 8 9 |
if (is_feed()) { function feedFilter($query) { if ($query->is_feed) { $query->set('post_type','any'); } return $query; } add_filter('pre_get_posts','feedFilter'); } |
1 2 3 4 5 6 7 8 9 10 11 12 |
if (is_feed()) { function feedFilter($query) { if ($query->is_feed) { $query->set('posts_per_page','11'); } return $query; } add_filter('pre_get_posts','feedFilter'); } |
1 2 3 4 5 6 7 8 9 10 |
if (is_feed()) { function feedFilter($query) { if ($query->is_feed) { $query->set('category_name', 'my-special-cat'); } return $query; } add_filter('pre_get_posts','feedFilter'); } |
After some downtime, GreenApe is breathing again. I revived the brand with a good friend of mine and we will be reopening shop options shortly.
Michael and I have been friends for a long time. We have been working on many different projects over the years.
He launched GreenApe in 2011 and I helped him with his first steps. A couple of months ago we decided to merge our competences and expand what GreenApe offers and stands for.
From the website: “The GreenApe brand was established in 2011. GreenApe’s career began with the 1st Single Malt Whisky Coffee.
As the first of its kind, our coffee is refined with Original Single Malt Whisky. To this day, he pampers many connoisseurs and gourmets with his unique taste. Now there is another reason to rejoice.
From now on, we are continuously expanding the GreenApe product world with several stylish gadgets and useful accessories. For you this means that you will be able to discover even more beautiful, special or practical things in the future.“
GreenApe is all about lifestyle & leisure products, fun gadgets and unique food & drinks.
“wp-password-bcrypt is a WordPress plugin to replace WP’s outdated and insecure MD5-based password hashing with the modern and secure bcrypt.”
Paw is a full-featured HTTP client that lets you test the APIs you build or consume. It has a beautiful native OS X interface to compose requests, inspect server responses and generate client code out-of-the-box.
This is one of my go-to tools, when test-driving my API endpoints.
When performing tasks using the Gravity Forms API, handling notifications and hook execution on demand is something that becomes really handy.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
<?php trait GravityForm{ function GravityForm_send_notifications($form_id, $entry_id){ // Get the array info for our forms and entries // that we need to send notifications for $form = RGFormsModel::get_form_meta($form_id); $entry = RGFormsModel::get_lead($entry_id); // Loop through all the notifications for the // form so we know which ones to send $notification_ids = array(); foreach($form['notifications'] as $id => $info){ array_push($notification_ids, $id); } // Send the notifications GFCommon::send_notifications($notification_ids, $form, $entry); } function GravityForm_execute_hooks($form_id, $entry_id){ $form = GFAPI::get_form( $form_id ); $entry = GFAPI::get_entry( $entry_id ); $registered_addons = GFAddOn::get_registered_addons( ); foreach( $registered_addons as $reg){ if(method_exists($reg, "get_instance")){ $execute = $reg::get_instance(); foreach($registered_addons as $addon){ $execute->maybe_process_feed( $entry, $form); } } } return true; } } |
Visual Composer shortcodes are normally not converted within the WordPress REST API rendered output.
This can be accomplished by calling WPBMap::addAllMappedShortcodes();
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
add_action( 'rest_api_init', function () { register_rest_field( 'page', 'content', array( 'get_callback' => 'convert_do_shortcodes', 'update_callback' => null, 'schema' => null, ) ); }); function convert_do_shortcodes( $object, $field_name, $request ) { WPBMap::addAllMappedShortcodes(); // This does all the work global $post; $post = get_post ($object['id']); $output['rendered'] = apply_filters( 'the_content', $post->post_content ); return $output; } |