Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
Get meta descriptions in The SEO Framework from ACF flexible content fields
* The SEO Framework + ACF flexible content integration
* TSF will look at the excerpt and then the content to generate the default meta description.
* If both of those are empty, this code looks for ACF flexible modules to get it from.
* // TODO: Make this work with archives as well as posts
* @param $description
* @param $args
* @return mixed|string
function doublee_seo_framework_description($description, $args) {
if(empty($description) && get_the_id()) {
$value_to_use = '';
// Find the first set of flexible modules, if there are any
$field_name = doublee_get_name_of_first_acf_field_of_type('flexible_content');
$modules = get_field($field_name);
// If there's modules, find the first one with a WYSIWYG or textarea field and get its value
if($field_name && $modules) {
$value_to_use = doublee_get_first_acf_subfield_value_of_type($modules, array('wysiwyg', 'textarea'), $field_name);
// Tell TSF to use this value; it will take care of truncating it for us.
$description = strip_tags($value_to_use);
return $description;
add_filter('the_seo_framework_custom_field_description', 'doublee_seo_framework_description', 10, 2);
add_filter('the_seo_framework_generated_description', 'doublee_seo_framework_description', 10, 2);
add_filter('the_seo_framework_fetched_description_excerpt', 'doublee_seo_framework_description', 10, 2);
* Utility function to get data about sub-fields that we want to use in doublee_get_first_acf_subfield_value_of_type
* because you can't use get_sub_field_object outside of an ACF have_rows loop which was causing headaches with nested repeaters and whatnot
* @param $field_name
* @param $post_id
* @return array
function doublee_get_sub_field_data($field_name, $post_id) {
global $wpdb;
$data = array();
// Query the database for the field content of this field's subfields. Starts with the field slug without an underscore.
// Returns an indexed array of meta ID, postt ID, meta key, and meta value sub-arrays.
$meta_key_search = "'" . $field_name . "%'";
$postmeta = $wpdb->get_results("SELECT * FROM $wpdb->postmeta WHERE post_id = $post_id AND meta_key LIKE $meta_key_search ORDER BY meta_key ASC", ARRAY_A);
// Query the database for the field keys of this field's subfields. Starts with the field slug preceded by an underscore.
// Field keys are not unique - e.g. repeaters will have the same field key for each instance of a subfield.
$meta_key_search = "'_" . $field_name . "%'";
$keymeta = $wpdb->get_results("SELECT * FROM $wpdb->postmeta WHERE post_id = $post_id AND meta_key LIKE $meta_key_search ORDER BY meta_key ASC", ARRAY_A);
// Merge the results
$merged = array();
foreach($postmeta as $index => $result_data) {
$merged[] = array_merge_recursive($result_data, $keymeta[$index]);
// Because the keys are the same in the arrays we merged, this will cause the values to be a sub-array
// Let's fix that, and don't include data we don't need
$flattened = array();
foreach($merged as $merged_array) {
$flattened[] = array(
'post_id' => $merged_array['post_id'][0],
'value' => $merged_array['meta_value'][0],
'key' => $merged_array['meta_value'][1]
// Use this and some more processing to build an array of all the data we need
$i = 0;
foreach($flattened as $index => $raw_data) {
if(is_array($raw_data)) {
$object = get_field_object($raw_data['key']);
$value = $raw_data['value'];
$parent_key = $object['parent'];
$parent_name = '';
if(!empty($parent_key)) {
$parent_object = get_field_object($parent_key);
$parent_name = $parent_object['name'];
$data[$i]['name'] = $object['name'];
$data[$i]['value'] = $value;
$data[$i]['type'] = $object['type'];
$data[$i]['parent_name'] = $parent_name;
return $data;
* Utility function to get the name of the first ACF field of the specified type.
* @param $field_type
* @param string $post_id
* @return int|string
function doublee_get_name_of_first_acf_field_name_of_type($field_type, $post_id = '') {
$field_name = '';
if(!$post_id) {
$post_id = get_the_id();
$acf_fields = get_fields($post_id, true);
if($acf_fields) {
foreach($acf_fields as $name => $value) {
$field_object = get_field_object($name);
if($field_object['type'] == $field_type) {
$field_name = $name;
return $field_name;
* Utility function to get the first direct subfield in an ACF set (flexible content or repeater) that is of the given type(s)
* Recursively checks within nested sets for their first instance of the type when applicable
* Returns the field (or sub-field) value ready for use by the calling function.
* // TODO: Test this on grouped fields too.
* @param array $fields Array of ACF fields or subfields, as returned by get_field() on a flexible content or repeater field
* @param array $types The field types we want to look for
* @param string $parent_field_name The name of the top level field, e.g. the flexible content field.
* Optional because when looking at nested fields recursively, the original value needs to be passed again.
* @return string
function doublee_get_first_acf_subfield_value_of_type(array $fields, array $types, string $parent_field_name = '') {
$all_field_data = doublee_get_sub_field_data('content_modules', get_the_id());
// If no fields were provided if they're not an array, bail early
// Brought this out on its own to keep the main loop's nesting as simple and shallow as possible
if(empty($fields) && !is_array($fields)) {
return false;
// Loop through the fields
foreach($fields as $index => $subfield) {
// If the subfield's value is an array, it's a nested fieldset so we need to go another level down
if(is_array($subfield)) {
return doublee_get_first_acf_subfield_value_of_type($fields[$index], $types, $parent_field_name);
// We've reached content fields and can now proceed to look for our desired field types
foreach($all_field_data as $data) {
if(($data['name'] == $index) && (in_array($data['type'],$types)) && (!empty($data['value']))) {
return $value_to_use = $data['value'];
// If a value hasn't been returned yet, there isn't one
return false;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment