I have a custom post type with page attributes so I can create sub pages. I'm trying to display the title of the parent page in the h1 and then have it loop through and display the content of the child pages. The code below almost does this but it's outputting the child pages also in the first bit as h1 titles so I'm getting duplicates of the child page titles. How can I exclude the child pages and prevent them from displaying in the first part of the loop?
Many thanks.
<?php
echo "<ul>";
if ( have_posts() ) {
while ( have_posts() ) {
the_post();
echo "<li><h1>".get_the_title()."</h1>";
$args=array(
'orderby' => 'menu_order',
'order' => 'ASC',
'post_parent' => $post->ID,
'post_type' => get_post_type( $post->ID ),
'posts_per_page' => 10
);
$childpages = new WP_Query($args);
if($childpages->post_count > 0) { /* display the children content */
echo "<ul>";
while ($childpages->have_posts()) {
$childpages->the_post();
echo "<li><h2>".get_the_title()."</h2></li>";
echo "<li><h2>".the_content()."</h2></li>";
}
echo "</ul>";
}
wp_reset_query();
echo "</li>";
}
}
echo "</ul>";
?>
Update: Managed to get a little bit further, almost there I think. I can now see just one sub post (the latest) and the same sub post is duplicated under each parent title either though it isn't a child of the others.
Can anyone please help me nail this last bit. Thanks.
<?php $parent_pages = get_pages( array(
'parent' => 0,
'post_type'=> 'archive'
));
foreach( $parent_pages as $parent_pages)
{ ?>
<h1><?php echo $parent_pages->post_title; ?></h1>
<?php
$children = get_pages(array(
'orderby' => 'menu_order',
'order' => 'ASC',
'post_parent' => $post->ID,
'post_type' => get_post_type( $post->ID )
));
foreach($children as $child);
?>
<h2><?php the_title(); ?></h2>
<?php the_content(); ?>
<?php } ?>
Update: Trying #robbintt suggestions, I'm now here. Not sure if I'm using the get_page_children correctly and I'm now getting "Parse error: syntax error, unexpected T_AS, expecting ';' "
<?php
$parent_pages = get_pages( array(
'parent' => 0,
'post_type'=> 'archive'
) );
for ( $parent_pages as $parent_page { ?>
<h1><?php echo $parent_page->post_title; ?></h1>
<?php
$all_pages = get_pages()
$child_pages = get_page_children($parent_pages->ID, $all_pages );
for ( $child_pages as $child_page ) { ?>
<h2><?php echo $child_page->post_title; ?></h2>
<p><?php echo $child_page->post_content; ?></p>
<?php } ?>
Update: Here's my final working code thanks to #robbintt for the help.
<?php
$parent_pages = get_pages( array( 'parent' => 0, 'post_type'=> 'archive' ) );
foreach ( $parent_pages as $parent_page ) {
echo '<h1>';
echo $parent_page->post_title;
echo '</h1>';
$all_pages = get_pages(array( 'post_type'=> 'archive' ) );
$child_pages = get_page_children($parent_page->ID, $all_pages );
foreach ( $child_pages as $child_page ) {
echo '<h2>';
echo $child_page->post_title;
echo '</h2>';
echo '<p>';
echo $child_page->post_content;
echo '</p>';
}
}
?>
You're abusing the loop a little bit here by using the standard while ( have_posts() ) {}.
Instead, lets target the parent page, and then use a for loop to get the child pages.
/* here we include only pages with no parent */
$parent_pages = get_pages( 'parent' => 0 )
foreach ( $parent_pages as $parent_page ) {
/* Proceed as you have inside the while loop, targeting $parent_page each time */
}
Here is the rest of the documentation so you can sort as you wish: http://codex.wordpress.org/Function_Reference/get_pages#Parameters
Part 2:
Here's the next requested section, how to get information for child pages:
http://codex.wordpress.org/Function_Reference/get_page_children
This function will return an array of page children. The key here is that this loop is run inside the other loop which actually gets all the parent pages.
$all_pages = get_pages( array( 'post_type'=> 'archive' ) )
$child_pages = get_page_children($parent_page->ID, $all_pages );
foreach ( $child_pages as $child_page ) {
/* proceed with any calls on child page such as ID/title, in the format $child_page->ID */
Let me know how it goes! Here's the new function we are using:
http://codex.wordpress.org/Function_Reference/get_page_children
Part Three:
Here I've cleaned up your code a bit for the child page section. There was a lot going on, so I standardized and simplified how you echo'd HTML and added a lot more lines. This made the design pattern a lot more visible to the human eye.
<?php
$parent_pages = get_pages( array( 'parent' => 0, 'post_type'=> 'archive' ) );
foreach ( $parent_pages as $parent_page ) {
echo '<h1>';
echo $parent_page->post_title;
echo '</h1>';
$all_pages = get_pages( array( 'post_type'=> 'archive' ) );
$child_pages = get_page_children($parent_page->ID, $all_pages );
foreach ( $child_pages as $child_page ) {
echo '<h2>';
echo $child_page->post_title;
echo '</h2>';
echo '<p>';
echo $child_page->post_content;
echo '</p>';
}
}
?>
Here's my original writeup of this code: http://codepad.org/pLtFCI1l
Related
I have the following shortcode that I am using to display a custom post on a page:
add_shortcode( 'page-section', 'page_section_shortcode' );
function page_section_shortcode( $atts ) {
global $post;
$post_slug=$post->post_name;
$a = shortcode_atts( array(
'post-name' => 'qwerty',
'bg-color' => 'white'
), $atts );
$post_slug=$post->post_name;
$post_name = $a['post-name'];
$query = new WP_Query( array(
'post_type' => 'page_section',
'name' => $post_name,
) );
if ( $query->have_posts() ) { ?>
<?php while ( $query->have_posts() ) : $query->the_post(); ?>
<div style="background-color: <?php echo $a['bg-color']; ?>" id="<?php global $post; $post_slug=$post->post_name; echo $post_slug; ?>" class="page-section">
<div class="row">
<?php the_content(); ?>
</div>
</div>
<?php endwhile;
wp_reset_postdata(); ?>
<?php $myvariable = ob_get_clean();
return $myvariable;
}
}
Everything is working the way I want it to, I can set the content in my custom post and use a short code to pull that content into a page.
The problem is that the content that is pulled in with the short code is always at the top of the page. Using multiple shortcodes in a row they keep their order, but any other content on the page is displayed at the bottom (below all of the shortcode content).
I have tried removing the 'echo' as suggested in another stackoverflow post but cannot seem to find what I am doing wrong.
Replace your code to this code. This code returns generated html so it will not echo at the top of the page
add_shortcode( 'page-section', 'page_section_shortcode' );
function page_section_shortcode( $atts ) {
global $post;
$post_slug=$post->post_name;
$a = shortcode_atts( array(
'post-name' => 'qwerty',
'bg-color' => 'white'
), $atts );
$post_slug=$post->post_name;
$post_name = $a['post-name'];
$query = new WP_Query( array(
'post_type' => 'page_section',
'name' => $post_name,
) );
$returnhtml = '';
if ( $query->have_posts() ) {
while ( $query->have_posts() ) : $query->the_post();
global $post;
$returnhtml .= '<div style="background-color: '. $a['bg-color'].'" id="'.$post->post_name.'" class="page-section">';
$returnhtml .= '<div class="row">'.get_the_content().'</div>';
$returnhtml .= '</div>';
endwhile;
wp_reset_postdata();
return $returnhtml;
}
}
function header_notification()
{
echo '<div><strong>Any html goes here</strong></div>';
}
add_action('wp_head', 'header_notification');
I've created a shortcode that displays my Staff post. Each post is floating left and there should be four post on each row. Everything is working fine but I need to add a clear div after every four post since the height may vary depending on the content in each post. How do I add the clear div after every four post in my shortcode? My code is below:
/*staff category start*/
add_shortcode( 'staff_category', 'staff_category' );
function staff_category( $atts ) {
ob_start();
// define attributes and their defaults
extract( shortcode_atts( array (
'type' => 'staff',
'order' => 'ASC',
'orderby' => 'menu_order',
'posts' => -1,
'staff_category' => '',
'category' => '',
), $atts ) );
// define query parameters based on attributes
$options = array(
'post_type' => $type,
'order' => $order,
'orderby' => $orderby,
'posts_per_page' => $posts,
'stafftype' => $staff_category,
'category_name' => $category,
);
$query = new WP_Query( $options );
// run the loop based on the query
if ( $query->have_posts() ) { ?>
<?php while ( $query->have_posts() ) : $query->the_post(); ?>
<article class="fourth-col-center staff-profile">
<?php
if(has_post_thumbnail()) { ?>
<div class="staff-img">
<?php the_post_thumbnail( 'staff-thumb' ); ?>
</div>
<?php } else { ?>
<div class="staff-img">
<?php echo '<img src="' . get_bloginfo( 'stylesheet_directory' ) . '/images/nophoto.jpg" />'; ?>
</div>
<?php } ?>
<h3><?php the_title(); ?></h3>
<div class="loop-post-meta">
<ul>
<li>
<?php
global $post;
$stafftitle_value = get_post_meta($post->ID, "wpstaff_textfield", true);
if($stafftitle_value){ ?>
<?php echo $stafftitle_value ?>
<?php } ?>
</li>
</ul>
</div>
</article>
<?php endwhile;
wp_reset_postdata(); ?>
<div class="cleared"></div>
<?php $myvariable = ob_get_clean();
return $myvariable;
}
}
/*staff category end*/
You could use CSS, something like this should work:
article.staff-profile:nth-child(4n+5) {
clear: both;
}
Have a shortcode thats calling a custom post type to display on an area in a standard loop template.
problem I'm having is that the output is appearing above the what's in the page.
Can anyone help?
Code is:
function JDD_display_stores() {
ob_start();
$args = array(
'post_type' => 'stores',
'tax_query' => array(
array(
'taxonomy' => 'store',
'field' => 'slug'
)
)
);
$success = new WP_Query( $args );
if( $success->have_posts() ) {
while( $success->have_posts() ) {
$success->the_post();
?>
<h1><?php the_title() ?></h1>
<div class='content'>
<?php the_content() ?>
</div>
<?php
return $success;
}
}
else {
echo 'No stores have been added!';
}
}
add_shortcode('display_stores', 'JDD_display_stores');
You'll need return the titles and content rather than echoing it
(which the_content() and the_title() do)
like:
$output = '';
while( $success->have_posts() ) {
$success->the_post();
$output .= sprintf("<h1>%s</h1>", get_the_title());
$output .= sprintf('<div class="content">%s</div>', get_the_content());
}
//reset the orignal main query
//see http://codex.wordpress.org/Function_Reference/wp_reset_query
wp_reset_query();
return $output;
..you'll need get_the_title() and get_the_content() for that.
Also take care, that you put the return outside the while loop, otherwise you'll exit the function in the first iteration...
see http://codex.wordpress.org/Function_Reference/get_the_content
& http://codex.wordpress.org/Function_Reference/get_the_title
I am building a shopping website and I am trying to put a shortcode in that will show the customer a buy button and the quantity of the product the customer wants to purchase. On my post page the shortcode works fine:
http://warringah-plastics.com.au/blog/dt_catalog/recess-gasket-large/
but on the archive page:
http://warringah-plastics.com.au/store/
the shortcode id displayed as text and not the actual button and quantity e.g. [add_to_cart item="FPROWAR-160713-1" showprice="no" quantity="user:1" ajax="yes" ].
The code that works in the post page is this:
<?php
$my_textbox_value = mtbxr_val("shopping_shortcode");
echo do_shortcode("$my_textbox_value");
?>
but it just displays the shortcode text on that archive page. Anyone have any ideas? Much appreciated,
UPDATE
THIS IS THE CODE THAT DISPLAYS THE SHORTCODE CORRECTLY:
<?php get_header(); ?>
<?php dt_storage('have_sidebar', true); ?>
<?php get_template_part('top-bg'); ?>
<?php get_template_part('parallax'); ?>
<div id="wrapper">
<?php get_template_part('nav'); ?>
<div id="container">
<?php if( have_posts() ): while( have_posts() ): the_post(); ?>
<h1><?php the_title(); ?></h1>
<h1 style="color: #3C3C3B !important; margin-top:-20px !important;"><?php $terms_as_text = strip_tags( get_the_term_list( $wp_query->post->ID, 'dt_catalog_category', '', ', ', '' ) );
echo $terms_as_text; ?></h1>
<?php
global $post;
$post_opts = get_post_meta($post->ID, '_dt_catalog-post_options', true);
if( !isset($post_opts['hide_media']) || (isset($post_opts['hide_media']) && !$post_opts['hide_media']) ) {
$args = array(
'post_type' => 'attachment',
'post_status' => 'inherit',
'posts_per_page' => -1,
'post_parent' => $post->ID,
'post_mime_type' => 'image',
'orderby' => 'menu_order',
'order' => 'ASC'
);
if( !empty($post_opts['hide_thumbnail']) )
$args['post__not_in'] = array( get_post_thumbnail_id() );
$dt_tmp_query = new WP_Query( $args );
if( $dt_tmp_query->have_posts() ) {
$slides = array();
foreach( $dt_tmp_query->posts as $slide ) {
$video = get_post_meta( $slide->ID, '_dt_catalog_video_link', true );
$tmp_arr = array();
$tmp_arr['caption'] = $slide->post_excerpt;
if ( ! $video ) {
$slide_src = dt_get_resized_img( wp_get_attachment_image_src( $slide->ID, 'full' ), array( 'w' => 710 ) );
$tmp_arr['alt'] = get_post_meta( $slide->ID, '_wp_attachment_image_alt', true );
$tmp_arr['src'] = $slide_src[0];
$tmp_arr['size_str'] = $slide_src[3];
} else {
$tmp_arr['is_video'] = true;
$tmp_arr['src'] = $video;
$tmp_arr['size_str'] = array( 710, 1024 );
}
$slides[] = $tmp_arr;
}
dt_get_anything_slider( array( 'id' => 'slider2', 'items_arr' => $slides ) );
}
}
?>
<?php $opts = get_post_meta($post->ID, '_dt_catalog-goods_options', true); ?>
<?php if( !empty($opts['price']) ): ?>
<span class="price"><?php _e('Price: ', LANGUAGE_ZONE); echo esc_html($opts['price']); ?></span>
<?php endif; ?>
<?php
$my_textbox_value = mtbxr_val("shopping_shortcode");
echo do_shortcode("$my_textbox_value");
?>
<?php
the_content();
if( dt_is_page_soc_buttons_enabled('catalog') ) {
dt_get_like_buttons( get_the_ID() );
}
?>
<?php if( !empty($opts['p_link']) ): ?>
<span><i class="dol"></i><?php _e('Make purchase!', LANGUAGE_ZONE); ?></span>
<?php endif; ?>
<p class="gap"></p>
<?php
$rel_works = get_post_meta($post->ID, '_dt_catalog_related', true);
if( isset($rel_works['show_related']) && $rel_works['show_related'] ):
if( 'same' == $rel_works['related'] ) {
$rel_works['related'] = wp_get_post_terms(
$post->ID,
'dt_catalog_category',
array('fields' => 'ids')
);
}
if( !empty($rel_works['related']) ):
?>
<p class="hr hr-narrow gap-small"></p>
<div class="gap"></div>
<div class="full-width w-photo">
<h2><?php _e('Related Items', LANGUAGE_ZONE); ?></h2>
<?php
if( 'same' == $rel_works['related'] ) {
$rel_works['related'] = wp_get_post_terms(
$post->ID,
'dt_catalog_category',
array('fields' => 'ids')
);
}
$dt_tmp_query = new WP_Query( array(
'posts_per_page' => -1,
'post_type' => 'dt_catalog',
'post_status' => 'publish',
'post__not_in' => array($post->ID),
'tax_query' => array( array(
'taxonomy' => 'dt_catalog_category',
'field' => 'id',
'terms' => $rel_works['related'],
'operator' => 'IN'
) )
) );
if( $dt_tmp_query->have_posts() ) {
$thumb_arr = dt_core_get_posts_thumbnails( $dt_tmp_query->posts );
$items = array();
foreach( $dt_tmp_query->posts as $rel_post ) {
$item = array();
$img = dt_get_resized_img(
dt_get_thumb_meta($thumb_arr['thumbs_meta'], 'full', $rel_post->ID),
array('w' => 196, 'h' => 123, 'use_noimage' => true)
);
$item['src'] = $img[0];
$item['size_str'] = $img[2];
$item['post_id'] = $rel_post->ID;
$item['desc'] = apply_filters('get_the_excerpt', $rel_post->post_excerpt);
$item['title'] = apply_filters('the_title', $rel_post->post_title, $rel_post->ID);
$item['alt'] = esc_attr( $item['title'] );
$items[] = $item;
}
$args = array( 'items_arr' => $items, 'id' => '', 'class' => 'list-carousel recent bx', 'ul_class' => 'slider1' );
$args['wrap'] = '<div class="%CLASS% bx">%SLIDER%</div>';
if( ! empty( $rel_works['show_desc'] ) || ! empty( $rel_works['show_title'] ) ) {
$title = '';
if( ! empty( $rel_works['show_title'] ) ) {
$title = '<h3>%TITLE%</h3>';
}
$desc = '';
if( ! empty( $rel_works['show_desc'] ) ) {
$desc = '<p>%DESC%</p>';
}
$args['item_wrap'] = '
<li>
<div class="textwidget">
<div class="textwidget-photo">
<a class="photo" href="%LINK%"><img src="%IMG_SRC%" alt="%ALT%" %IMG_SIZE% /></a>
</div>
<div class="widget-info">
<div class="info">
' . $title . $desc . '
</div>
</div>
</div>
</li>
';
}
dt_get_carousel_slider( $args );
}
?>
</div>
<?php endif; endif; ?>
<?php comments_template(); ?>
<?php
endwhile;
endif;
?>
</div>
<?php dt_widget_area('sidebar', null, 'sidebar_4'); ?>
</div>
<?php get_footer(); ?>
AND THIS IS THE CODE THAT DISPLAYS THE SHORTCODE JUST AS TEXT:
<?php
global $post;
$page_data = dt_storage( 'page_data' );
$page_opts = ! empty( $page_data['page_options'] ) ? $page_data['page_options'] : array();
$add_data = dt_storage( 'add_data' );
$first_class = '';
if( 1 === dt_storage('post_is_first') ) {
$first_class = ' first';
dt_storage( 'post_is_first', -1 );
}
$opts = get_post_meta($post->ID, '_dt_catalog-goods_options', true);
?>
<div class="<?php dt_portfolio_classes( '2_col-list', 'block' ); echo $first_class; ?>">
<?php
$h = 220;
if ( ! empty ( $page_opts['thumb_height'] ) ) {
$h = $page_opts['thumb_height'];
}
dt_get_thumb_img( array(
'class' => 'photo',
'use_noimage' => true,
'href' => get_permalink(),
'thumb_opts' => array( 'w' => 343, 'h' => $h )
),
'<div class="textwidget-photo">
<a %HREF% %CLASS% %TITLE% %CUSTOM%><img %ALT% %SRC% %IMG_CLASS% %SIZE% /></a>
</div>'
);
?>
<div class="<?php dt_portfolio_classes( '2_col-list', 'info' ); ?>">
<a class="<?php dt_portfolio_classes( '2_col-list', 'head' ); ?>" href="<?php the_permalink(); ?>"><?php the_title(); ?></a>
<?php if( !empty($opts['price']) ): ?>
<span class="price"><?php _e('Price: ', LANGUAGE_ZONE); echo esc_html($opts['price']); ?></span>
<?php endif; ?>
<?php
dt_the_content();
dt_details_link();
dt_edit_link();
?>
<div id="specialpriceshortcode">
<?php
$my_textbox_value = mtbxr_val("shopping_shortcode");
echo do_shortcode("$my_textbox_value");
?>
</div>
</div>
</div>
Try using single quotes in the do_shortcode call, like so:
echo do_shortcode('$my_textbox_value');
More likely though is that the shortcode isn't defined on the archive page so you'd need to look at where it is being instantiated to see if that is the issue. Normally when a shortcode just echoes out the content it means that shortcode doesn't exist. You can test easily enough by using the shortcode_exists() function:
<?php if ( shortcode_exists( 'add_to_cart' ) ) { echo "The shortcode exists";} ?>
If that doesn't work then you know the issue is with the shortcode not being registered on your archives page. If it does work then you know it's something with the format of the content being passed to the shortcode.
Add this to your functions.php
// Allow shortcodes on widgets
add_filter('widget_text','do_shortcode');
// Allow shortcodes on pages (not tested, but should work)
add_filter('the_content','do_shortcode');
Typically your shortcode is getting registered in a plugin or your theme's functions.php file. In a plugin it's often something like:
add_action('init', 'register_my_shortcode');
function register_my_shortcode(){
add_shortcode('my_shortcode', 'do_my_shortcode');
}
And then you'd have a function do_my_short_code() that actually outputs the content. With something like that the shortcode is getting registered via the 'init' hook (http://codex.wordpress.org/Plugin_API/Action_Reference) which is called before WP has started figuring out what template to use, what content to output, etc.
But some plugins will register the shortcode in a way that it is only available on pages / posts where it's going to potentially be used. For example, I can think of one plugin where they register the shortcode and enqueue some javascripts in the same function. That function checks to see if you're on a particular page before it executes so that the js files are not included unnecessarily all over the place. Since the shortcode registration takes place in the same function it means the shortcode only exists on those pages.
Anyhow, if the shortcode is showing as existing on your archives page you know that isn't the problem, so check that first and let me know what you find.
I'm using already designed theme for wordpress, and now instead of regular blog posts I would like to display WooCommerce products (which are custom post types I persume).
This is the current query with display loop:
<?php
$args = array(
//'posts_per_page' => '2',
'paged' => get_query_var('paged')
);
$homepage_query = new WP_Query($args);
?>
<?php //query_posts('posts_per_page=4&paged='.get_query_var('paged')); ?>
<?php if ( have_posts() ) : ?>
<?php while ( $homepage_query->have_posts() ) : $homepage_query->the_post(); ?>
<?php if($style == 'blog_style') { ?>
<div id="blog-style" class="post-box">
<?php get_template_part('content', 'blog'); ?>
</div>
<?php } else { ?>
<div class="post-box grid_4 <?php aero_post_box_class(); ?>">
<?php get_template_part('content', ''); ?>
</div>
<?php } ?>
<?php endwhile; ?>
Is there a way to add options to $args so the loop displays WooCommerce products? I'm also using pagination with this loop, which is required on this project, so that's why it's important to use this loop.
You should be able to access products through the loop, setting the post_type arg to product:
<?php
// Setup your custom query
$args = array( 'post_type' => 'product', ... );
$loop = new WP_Query( $args );
while ( $loop->have_posts() ) : $loop->the_post(); ?>
<a href="<?php echo get_permalink( $loop->post->ID ) ?>">
<?php the_title(); ?>
</a>
<?php endwhile; wp_reset_query(); // Remember to reset ?>
This is the proper way to re-create and customize the WooCommerce product loop:
if(!function_exists('wc_get_products')) {
return;
}
$paged = (get_query_var('paged')) ? absint(get_query_var('paged')) : 1; // if your custom loop is on a static front page then check for the query var 'page' instead of 'paged', see https://developer.wordpress.org/reference/classes/wp_query/#pagination-parameters
$ordering = WC()->query->get_catalog_ordering_args();
$ordering['orderby'] = array_shift(explode(' ', $ordering['orderby']));
$ordering['orderby'] = stristr($ordering['orderby'], 'price') ? 'meta_value_num' : $ordering['orderby'];
$products_per_page = apply_filters('loop_shop_per_page', wc_get_default_products_per_row() * wc_get_default_product_rows_per_page());
$products_ids = wc_get_products(array(
'status' => 'publish',
'limit' => $products_per_page,
'page' => $paged,
'paginate' => true,
'return' => 'ids',
'orderby' => $ordering['orderby'],
'order' => $ordering['order'],
));
wc_set_loop_prop('current_page', $paged);
wc_set_loop_prop('is_paginated', wc_string_to_bool(true));
wc_set_loop_prop('page_template', get_page_template_slug());
wc_set_loop_prop('per_page', $products_per_page);
wc_set_loop_prop('total', $products_ids->total);
wc_set_loop_prop('total_pages', $products_ids->max_num_pages);
if($products_ids) {
do_action('woocommerce_before_shop_loop');
woocommerce_product_loop_start();
foreach($products_ids->products as $featured_product) {
$post_object = get_post($featured_product);
setup_postdata($GLOBALS['post'] =& $post_object);
wc_get_template_part('content', 'product');
}
wp_reset_postdata();
woocommerce_product_loop_end();
do_action('woocommerce_after_shop_loop');
} else {
do_action('woocommerce_no_products_found');
}
Using the code above, you would customize the wc_get_products() arguments to get the IDs of the products you want (if you have specific criteria). Once that code is in place, all the features of a native WooCommerce loop will be available to you—pagination, ordering, etc. This method is superior to WP_Query and get_posts() because those two methods can break.
I've written a more detailed blog post about custom WooCommerce loops here: https://cfxdesign.com/create-a-custom-woocommerce-product-loop-the-right-way/
You can Also get Category using thi code
$terms = get_terms('product_cat');
foreach ($terms as $term) {
$term_link = get_term_link( $term, 'product_cat' );
echo '<li>' . $term->name . '</li>';
}
If You want only parent category then
wp_list_categories('taxonomy=product_cat&orderby=order&title_li=&depth=1');