Closed1

Qwik + vite + express, CMS表示系の作成

knaka Tech-Blogknaka Tech-Blog

概要

Qwik + express SSR(react-dom/server)で、CMS表示のメモになります。

  • 主に、SSRになります。一部CSRになります。
  • 編集機能は、前の複数blog対応 Headless CMSと連携します (CF-pages)
  • テストは、vercelにデプロイする形です。

[ 公開: 2024/03/02 ]


環境

  • Qwik
  • express
  • vite: 5
  • esbuid
  • vercel

関連

  • 前のexpress 構成関連です。

https://zenn.dev/knaka0209/books/b1bd883fb8dd95


作成したコード

https://github.com/kuc-arc-f/express_40qwik/tree/main/sample/blog


  • 詳細 : MD変換ライブラリ使います。

  • pages/posts/show/App.tsx

App.tsx
export default  function PostShow(props: any) {
  console.log("#taskShow");
//console.log(props);
  const content = marked.parse(props.item.content);
//console.log(content);
  //
  return (
  <Layout title="Show">
    <div className="post_show_wrap container bg-white mx-auto mt-14 mb-8 px-8 py-4">
      <a href="/" className="btn-outline-purple ms-2 my-0"
      >back</a>
      <hr className="my-4" />
      <div id="root"></div>
      <h1 className="text-4xl font-bold">{props.item.title}</h1>
      <p>id: {props.item.id}, {props.item.createdAt}</p>
      <hr />
      <div dangerouslySetInnerHTML={{ __html: content }} id="content_html"
      className="mb-8" />
      <hr className="my-12" />
    </div>
    <style>{`
    .post_show_wrap {min-height : 600px;}
    `}
    </style>
  </Layout>
  )
};

  • Top: pages/posts/App.tsx
App.tsx
export default function Page(props: any) {
//console.log(props.site);
  if(props.page){
    nextPage = Number(props.page) + 1;
    beforePage = Number(props.page) - 1;
    if(beforePage <= 1) { beforePage = 1;}
  }
  //
  return (
  <Layout>
    <div className="text-center py-16 bg-gray-400 text-white mt-10">
      <h1 className="text-4xl font-bold">{props.site.name}</h1>
    </div>
    <input type="text" className="d-none" id="item_id" defaultValue={props.id} />
    <div className="col-md-6 text-end  bg-white py-1">
        <span className="search_key_wrap">
        <input type="text" 
        className="mx-2 border border-gray-400 rounded-md px-3 py-2 focus:outline-none focus:border-blue-500" 
        name="searchKey" id="searchKey"
        placeholder="Title search" />
        </span>                
        <button className="ms-2 btn-outline-purple" id="btn_search"
        >Search</button>
    </div>
    {/* post_list_wrap */}
    <div className="post_list_wrap container mx-auto my-2 px-2">
      {props.items.map((item: any) => {
        return (
        <div key={item.id} className="rounded-md bg-white my-2  p-4">
          <div className="flex flex-row">
            <div className="flex-1 p-2 m-1">
              <a href={`/posts/${item.id}`}><h3 className="text-3xl font-bold"
              >{item.title}</h3></a>
              <p>ID: {item.id}, {item.createdAt}</p>
            </div>
            <div className="flex-1 p-2 m-1 text-end">
              <a href={`/posts/${item.id}`}>
                <button  className="btn-outline-purple ms-2 my-2">Show</button>
              </a>
            </div>
          </div>
        </div>
        );
      })}
    </div>
    <div id="app"></div>        
    {(process.env.NODE_ENV === "develop") ? (
        <script type="module" src="/static/Top.js"></script>
    ): (
        <script type="module" src="/public/static/Top.js"></script> 
    )}
    <hr className="my-8" />
    <style>{`
    .post_list_wrap {min-height: 500px;}
    `}</style>
      
  </Layout>
  )
}

  • src/index.ts
  • headless CMS APIと、連携します。
  • API接続先は、 .envから取得します。
index.ts
import express from 'express';
import { renderToString } from 'react-dom/server';

const app = express();

....
import PostsIndex from './pages/posts/App';
import PostsShow from './pages/posts/show/App';

app.get('/',async (req: any, res: any) => {
  try { 
    const site = await siteRouter.get();
    const items = await postRouter.get_list_page(Number(1));
    res.send(renderToString(PostsIndex({items: items, page: 1, site: site}))); 
  } catch (error) { res.sendStatus(500); }
});

  • front : client/Top/app.tsx, 検索部分
  • 外部API連携、検索してます。
app.tsx
import { component$, useSignal, useStore, useComputed$, useTask$, $ } from '@builder.io/qwik';

....

export const App = component$(() => {
  const text = useSignal('qwik');
  const state = useStore({ count: 0, items: [] });
  const count = useSignal(0)
  const time = useSignal('paused');
  //init
  useTask$(({ track, cleanup }) => {
    const btn_search = document.querySelector('#btn_search');
    btn_search?.addEventListener('click', async () => {
        const post_list_wrap = document.querySelector(`.post_list_wrap`) as HTMLInputElement;
        if (!post_list_wrap.classList.contains('d-none')) {
            post_list_wrap?.classList.add('d-none');
        }
        const res = await CrudIndex.search();
        state.items = res;
      console.log(res);
    });
  });
  //
  return (
  <>
    <div class="search_result_wrap container mx-auto my-2 px-2">
      <hr class="my-2" />
      {state.items.map((item: any) => {
        return (
        <div key={item.id} class="rounded-md bg-white my-2  p-4">
          <div class="flex flex-row">
            <div class="flex-1 p-2 m-1">
              <a href={`/posts/${item.id}`} target="_blank"><h3 class="text-3xl font-bold"
              >{item.title}</h3></a>
              <p>ID: {item.id}, {item.createdAt}</p>
            </div>
            <div class="flex-1 p-2 m-1 text-end">
              <a href={`/posts/${item.id}`} target="_blank">
                <button  class="btn-outline-purple ms-2 my-2">Show</button>
              </a>
            </div>
          </div>
        </div>
        );
      })}
    </div>
  </>
  )
})

.env

VITE_SITE_ID=1
VITE_API_URL=http://localhost
VITE_API_KEY="123"
このスクラップは18日前にクローズされました